Architecture validation service

This container is the runtime side of the webathome.org architecture metaschema. It hosts the v0.1 JSON Schemas, validates submitted architecture artifacts via POST /api/validate, and serves the diagram viewer at /viewer/.

Producers (Ansible, the deploy repos, EI, IoT, …) emit one architecture.yaml per build. Each producer's CI runs arch-validate against this service, fails the build on non-zero exit, and archives the artifact for the Architecture pipeline's collector to pick up.

POST /api/validate

POST https://architecture.webathome.org/api/validate
Content-Type: application/json | application/yaml | text/yaml
Body: artifact in the matching format (≤ 5 MiB)

The artifact's top-level schemaVersion selects the validator. v0.1 accepts only "0.1"; any other value returns 400.

status meaning
200 with {"valid":true,...} artifact passes schema + triple matrix
200 with {"valid":false,"errors":[...]} one or more validation errors
400 unparseable body, missing/unknown schemaVersion
415 Content-Type other than the three above
500 server bug

HTTP status is for request handling. The valid field is the validation outcome — both pass and fail return 200.

Success

{
  "valid": true,
  "schemaVersion": "0.1"
}

Failure

Each error is normalised to a single LLM-friendly shape. The path is a JSON Pointer into the submitted artifact, line is the 1-based line it sits on in the submitted text (the key's line for a mapping value; absent when the pointer does not map to a node, e.g. through a << merge), the value is extracted at that path so the reader doesn't have to re-fetch it, and schemaUrl points at the most specific schema or enum file relevant to the error.

{
  "valid": false,
  "schemaVersion": "0.1",
  "errors": [
    {
      "path": "/nodes/0/id",
      "line": 5,
      "keyword": "pattern",
      "message": "value 'Node_BadId' does not match the required pattern /^node:[a-z][a-z0-9-]*,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/",
      "value": "Node_BadId",
      "schemaUrl": "https://architecture.webathome.org/schema/v0.1/generated/node.schema.json",
      "hint": "see the schema's pattern for the exact rule"
    }
  ]
}

A type error on a scalar quotes the scalar as written next to what the YAML 1.2 reader made of it, so a string that parsed as a number is visible as such: firmware: 9e10234 (parsed as float Infinity) is not of expected type string. A long or multi-line scalar is cut to its first line, at most 60 characters, ending in ….

The service also enforces the ArchiMate 3.2 (source-kind, type, target-kind) matrix on every relation whose endpoints are present in the submitted artifact. Cross-producer references (ids not present locally) are skipped — the Architecture pipeline's collector enforces them at merge time. Matrix violations are reported with keyword: "x-allowedTriples".

Merged-dataset endpoints

The Architecture pipeline runs the federation collector (tooling/collect.py) during the image build, merges every registered producer's last-successful architecture.yaml into a single consolidated dataset, and bakes the three output files into the container. The validation service serves them verbatim from disk.

URL content
/data/v0.1/architecture.yaml merged dataset (YAML) — every element kind, every relation, derived groupings and capabilityRealizations maps
/data/v0.1/architecture.json same content, canonical JSON
/data/v0.1/validation-report.json summary + warnings[] + divergences[] for the last successful pipeline run

Errors fail the pipeline before emission, so the report never carries errors — only non-fatal observations (deprecated-target references, alias-hint divergence across producers, etc.). The Jenkinsfile also archives validation-report.json as a Jenkins build artifact for historical traceability.

Schema URLs

Every schema is served at both .yaml (the canonical authoring form) and .json (the canonical JSON Schema form). Use whichever your tooling prefers.

URL content
/schema/v0.1/architecture.schema.yaml top-level artifact envelope
/schema/v0.1/subset.schema.yaml meta-schema for subset.yaml
/schema/v0.1/generated/<kind>.schema.yaml per-kind element schemas (node, applicationcomponent, …)
/schema/v0.1/generated/relations.schema.yaml relation schema + x-allowedTriples matrix
/schema/v0.1/enums/capabilities.yaml curated capability catalogue
/schema/v0.1/enums/lifecycle-states.yaml lifecycle enum
/schema/v0.1/enums/environments.yaml environment enum
/schema/v0.1/archimate/archimate3_Model.xsd vendored ArchiMate 3.2 XSD
/schema/v0.1/archimate/relationships.xml Archi relationship matrix (source)

All responses set Access-Control-Allow-Origin: * and Cache-Control: public, max-age=300.

End-to-end curl example

cat > artifact.yaml <<'EOF'
schemaVersion: "0.1"
producer: my-repo

systemSoftware:
  - id: ss:my-daemon,3f1b9c40-0e2a-4d11-9f7a-2c6b1e8a4d90
    label: My daemon
    summary: Long-running service owned by my-repo.
    introduced: 2026-05-27
    lifecycle: active
EOF

curl -sS \
  -H 'Content-Type: application/yaml' \
  --data-binary @artifact.yaml \
  https://architecture.webathome.org/api/validate \
  | jq .

arch-validate CLI

The dev-facing artifact. Producers run it from the aac-tools toolchain, the registry:5000/aac-tools image ArgoCDTools publishes: in Jenkins through the JenkinsPipelineUtils architectureProducer steps, which run it in the toolchain's container (see the producer manual's Jenkins integration), in a KubeCoder environment that declares the toolchain through cexec aac-tools. A producer repo keeps no copy of the script.

# from a producer repo's KubeCoder environment
cexec aac-tools arch-validate architecture.yaml
cexec aac-tools arch-validate dev.architecture.yaml prd.architecture.yaml
cat architecture.yaml | cexec aac-tools arch-validate -      # stdin
cexec aac-tools arch-validate --json architecture.yaml       # raw endpoint JSON
cexec aac-tools arch-validate --quiet architecture.yaml      # suppress OK lines

Each error prints as its path, then its message — prefixed line N: when the error carries a line — then its schemaUrl and any hint.

Exit codes: 0 valid, 1 invalid, 2 transport/server error.

Override the endpoint for local testing:

cexec aac-tools env ARCHITECTURE_VALIDATE_URL=http://localhost:8080/api/validate \
  arch-validate artifact.yaml

The toolchain ships this repo's .claude/architecture/arch-validate.py byte for byte: ArgoCDTools vendors it as aac-tools/image/arch-validate.py, and a test there pins its md5. A change here reaches producers when ArgoCDTools takes the new copy and republishes the image.

$schema pragma

For editor / IDE schema-completion (VS Code's YAML extension, IntelliJ's JSON Schema mappings, etc.), reference the envelope schema at the top of your artifact:

# yaml-language-server: $schema=https://architecture.webathome.org/schema/v0.1/architecture.schema.json
schemaVersion: "0.1"
producer: my-repo
# …

The schema URL is stable; only the v0.1 immutability rule applies.

Keeping an artifact current

The architecture in a producer repo is kept current centrally, from the Architecture repo, rather than by the producer repo itself. On demand, a tool there clones each registered repo, judges whether the commits since its architecture sources last changed — or since that repo was last reviewed, whichever is later — change what the architecture must say, and where they do runs a headless session in the clone that edits the artifact and commits. Those commits are pushed to the repo's default branch, and the builds the push starts are followed to their end. A repo whose AaC job builds a promotion branch, such as a deploy repo's prd, which only fast-forwards along the default branch, gets the same push; its artifact picks the edit up when that branch is promoted.

A generated producer is also judged on the gaps its generator reports: the gap: <what> lines of its last successful AaC build. A gap no run has judged yet goes to the editing session even when no commit is new, and that session is handed every gap the build reports, whatever the commits.

Nothing is installed in a producer repo: the agents that do the editing are copied into the throwaway clone for the run, and the repo is not asked to invoke anything itself.

An optional .architecturerc at the producer repo's root steers that run:

generated: false                              # true for a generated producer
sources: [":(glob)**/docs/architecture/**"]   # git pathspecs the architecture is built from
instructions: |                               # handed verbatim to the sessions
  Model the worker pool as one component.

All three keys are optional: generated and sources default to the values shown, instructions to nothing. Any other key is an error. instructions wins over the agents' own guidance on anything specific to the repo. A generated producer does need the file: generated: true is what keeps the session in the sources the artifact is generated from, instead of the artifact it would otherwise edit.

Schema-change requests

To add a capability id, a relation kind, a stereotype, or any other vocabulary extension: open a PR against this repo (pvginkel/Architecture) editing the relevant file under schema/v0.1/:

After editing, run the generator and validator locally:

cd tooling && poetry run python generate.py
cd tooling && poetry run python validate.py meta
cd tooling && poetry run python validate.py ../schema/v0.1/examples/valid-full.yaml

CI on this repo runs the same checks plus the service's vitest suite.