diff --git a/beehive/go.mod b/beehive/go.mod index fecb9ce..79d9c0d 100644 --- a/beehive/go.mod +++ b/beehive/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/beehive -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/beekeeper/go.mod b/beekeeper/go.mod index 74a9da9..a836e2b 100644 --- a/beekeeper/go.mod +++ b/beekeeper/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/beekeeper -go 1.26.3 +go 1.26.4 require ( github.com/gorilla/websocket v1.5.3 diff --git a/busdriver/go.mod b/busdriver/go.mod index e6e0c56..0c67b25 100644 --- a/busdriver/go.mod +++ b/busdriver/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/busdriver -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/ca/go.mod b/ca/go.mod index 639894d..71eca22 100644 --- a/ca/go.mod +++ b/ca/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/ca -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/clerk/go.mod b/clerk/go.mod index 211198f..40baea6 100644 --- a/clerk/go.mod +++ b/clerk/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/clerk -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/collector/go.mod b/collector/go.mod index 1ff1a2f..561c1ea 100644 --- a/collector/go.mod +++ b/collector/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/collector -go 1.26.3 +go 1.26.4 require ( github.com/influxdata/influxdb-client-go/v2 v2.14.0 diff --git a/democrat/go.mod b/democrat/go.mod index 7f22985..f63748a 100644 --- a/democrat/go.mod +++ b/democrat/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/democrat -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/drafter/go.mod b/drafter/go.mod index f315974..1be67b6 100644 --- a/drafter/go.mod +++ b/drafter/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/drafter -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/ds18b20/go.mod b/ds18b20/go.mod index e71b46e..b506f72 100644 --- a/ds18b20/go.mod +++ b/ds18b20/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/ds18b20 -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/emulator/go.mod b/emulator/go.mod index d7f2b3b..deafef1 100644 --- a/emulator/go.mod +++ b/emulator/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/emulator -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/esr/go.mod b/esr/go.mod index 51f0687..7bdcc84 100644 --- a/esr/go.mod +++ b/esr/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/esr -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/ethermostat/go.mod b/ethermostat/go.mod index a9d6e01..8945e0c 100644 --- a/ethermostat/go.mod +++ b/ethermostat/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/ethermostat -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/filmer/go.mod b/filmer/go.mod index a948579..bdea3e8 100644 --- a/filmer/go.mod +++ b/filmer/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/filmer -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/flattener/go.mod b/flattener/go.mod index 33b12b6..0b3174a 100644 --- a/flattener/go.mod +++ b/flattener/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/flattener -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/kgrapher/README.md b/kgrapher/README.md index a36e5e6..077570e 100644 --- a/kgrapher/README.md +++ b/kgrapher/README.md @@ -1,124 +1,298 @@ # mbaigo System: KGrapher ## Purpose -This system offers as a service the semantic model of its local cloud. -It obtains the list of systems from the Service Registrar and then asks each system for its model. -It can then aggregate them to represent the complete local cloud, a distributed system of systems, each with the assets it has and the services that are provided and consumed. -The asset of the system is GraphDB, a graph database that systematically collects data along with the relationships between the different data entities. -Currently, when the KGrapher’s only service is invoked from a web browser, it generates the knowledge graph of the local cloud, pushes it to the database and provides a text version to the web browser. +The *KGrapher* assembles a complete **OWL / RDF knowledge graph** of an +Arrowhead local cloud — a distributed system of systems — by collecting +the per-system RDF fragment from each registered system and merging them +into a single, coherent Turtle (TTL) document. The result is what other +systems and human consumers query when they want to reason about the cloud +as a whole. + +When a GraphDB triple store is configured, the KGrapher also **pushes** +the assembled graph as a SPARQL update so the latest snapshot is queryable +remotely. Locally it always serves the same TTL over HTTP for clients that +prefer to pull. + +## Architecture + +The KGrapher is an **aggregator system**, the OWL/RDF sibling of the +[modeler](../modeler/) (which plays the same role for SysML v2). Both +work the same way: discover every system via the Service Registrar, fetch +each system's per-system meta-view, deduplicate and merge, hand back the +combined result. Three responsibilities: + +1. **Collect.** Pull `//kgraph` from every registered system. Each + system already emits its own RDF block — the mbaigo framework's + `KGraphing` use case handles this. The KGrapher does no per-system + knowledge generation; it only orchestrates collection. +2. **Merge.** Deduplicate `@prefix` declarations, rewrite prefixes against + the local ontology files, prepend a cloud-wide IRI to per-system subjects, + and concatenate the resulting blocks into one TTL. +3. **Publish.** Return the merged TTL as the HTTP response. If + `graphDBurl` is set, also POST a SPARQL update that snapshots the graph + into GraphDB. + +The KGrapher is **demand-driven** — there is no background timer. Each +GET on `/cloudgraph` triggers a fresh collect + merge + push. Consumers +that want a current view ask for one; nothing is cached. + +## How it fits in the cloud + +```mermaid +sequenceDiagram + autonumber + actor User + participant K as KGrapher + participant SR as ServiceRegistrar + participant S as System + participant G as GraphDB + + User->>K: GET /kgrapher/assembler/cloudgraph + K->>SR: GET /serviceregistrar/registry/syslist + SR-->>K: SystemRecordList (list of base URLs) + + loop for each system + K->>S: GET //kgraph + S-->>K: Turtle fragment (@prefix + alc:System / afo:UnitAsset / afo:Service / …) + end + + K->>K: dedupe prefixes, rewrite via localOntologies,
prepend cloud IRI, concatenate + opt graphDBurl configured + K->>G: SPARQL UPDATE
CLEAR GRAPH ;
ADD GRAPH TO ; + end + K-->>User: merged TTL (text/turtle) +``` + +The push pattern is significant: each snapshot lands in its own named graph +keyed by IRI, and `` is rotated to always point at the +latest snapshot. Consumers querying `` always see the +current view; older snapshots persist in their own named graphs as a history +trail. + +## Services + +### Provided + +| Service definition | Subpath | Methods | Description | +|--------------------|---------|---------|-------------| +| `cloudgraph` | `cloudgraph` | `GET` | Assembles the cloud's RDF from every registered system, pushes it to GraphDB if configured, and returns the merged Turtle | +| `localOntologies` | `localontologies` | `GET` | HTML index of the ontology files the KGrapher serves locally, with links to download each | + +### Consumed (via Arrowhead orchestration) + +| Service definition | Used for | +|--------------------|----------| +| Pulled from every registered system via `//kgraph` | Per-system RDF fragments to merge | + +The KGrapher also POSTs to a configured **GraphDB SPARQL endpoint** — that +URL is in the trait file rather than discovered via Arrowhead, since GraphDB +isn't an mbaigo system. + +## Configuration + +```json +{ + "systemname": "kgrapher", + "unit_assets": [ + { + "name": "assembler", + "details": { "Type": ["Interactive"] }, + "services": [ + { + "definition": "cloudgraph", + "subpath": "cloudgraph", + "details": { "Format": ["Turtle"] }, + "registrationPeriod": 61 + }, + { + "definition": "localOntologies", + "subpath": "localontologies", + "details": { "Location": ["Files"] }, + "registrationPeriod": 61 + } + ], + "traits": [ + { + "graphDBurl": "http://:7200/repositories//statements", + "localOntologies": { + "alc": "alc-ontology-local.ttl" + } + } + ] + } + ], + "protocolsNports": { "coap": 0, "http": 20105, "https": 0 }, + "coreSystems": [ /* serviceregistrar, orchestrator, ca, maitreD */ ] +} +``` + +### Trait reference + +| Field | Type | Description | +|-------|------|-------------| +| `graphDBurl` | string | SPARQL **update** endpoint (note the `/statements` suffix). Empty disables remote publication; local TTL serving still works | +| `localOntologies` | `map[string]string` | Per-prefix file name. The KGrapher reads each file from its own directory, rewrites the corresponding `@prefix` in the assembled output, and serves the file content over HTTP so other systems can dereference it | + +## The push mechanism in detail + +When `graphDBurl` is set, each `/cloudgraph` GET ends with a two-statement +SPARQL update: + +```sparql +CLEAR GRAPH ; +ADD GRAPH > TO ; +``` + +- The snapshot IRI is generated at push time and never reused. +- The first push to a given snapshot IRI also inserts the actual triples + into that snapshot graph; subsequent rotations of `` + only move the pointer. +- Old snapshots stay in the store. They are not automatically pruned — + if you care about disk usage, drop old snapshot graphs explicitly with + a `DROP GRAPH` update. + +The advantage of this pattern over a flat *"replace all triples in graph X"* +update is that **historical state stays queryable** under each +timestamp-keyed graph IRI, while consumers who only care about "now" can +query `` and ignore the history. + +## Predicate emission and the FunctionalLocation special-case + +Per-system RDF emitters in mbaigo's `usecases/kgraphing.go` apply one +naming rule with one carve-out: + +- Most detail keys on a unit asset become `alc:has` triples — local + to the cloud's `alc:` namespace. +- **`FunctionalLocation`** is emitted as `afo:hasFunctionalLocation` — + in the AFO (Arrowhead Framework Ontology) namespace — so AFO-IDO, + AFO-DEXPI, and AFO-STEP alignment ontologies can bridge it to their + upstream vocabularies (e.g., `ido:locatedAt`). A cloud-local predicate + wouldn't participate in those alignments; the AFO predicate does. + +If you add other detail keys that warrant cross-ontology alignment, the +list lives in `mbaigo/usecases/kgraphing.go` and adding a new carve-out +is a one-line change. + +## Building and running + +```bash +# Run from source (development) +go run . + +# Build a binary for the current machine +go build -o kgrapher_amac . + +# Cross-compile for a 64-bit Raspberry Pi +GOOS=linux GOARCH=arm64 go build -o kgrapher_rpi64 . + +# Deploy +scp kgrapher_rpi64 jan@:oslo/kgrapher/ +``` -Using the model in conjunction with the Arrowhead Framework Ontology (afo), a computer can infer new knowledge about the local cloud and reason about it. +Run the binary from **inside its own directory** so it can find (or +auto-generate) `systemconfig.json` and read the `localOntologies` files. -The KGrapher also serves (makes available) local ontologies and knowledge graphs. -One can obtain these description through the *localOntologies* service. +## Startup order -## Compiling -To compile the code, one needs initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/kgrapher``` before running *go mod tidy*. +``` +Arrowhead core systems → KGrapher → any consumer +``` -To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. +The KGrapher is **demand-driven and stateless** — it pulls from the +registrar on each request, so application systems can join after it's +already running. The first `/cloudgraph` request after a system joins +will include that system's fragment automatically. -It is **important** to start the program from within its own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. +If `graphDBurl` points at a triple store that isn't yet up, the SPARQL +update will fail and the KGrapher will log the error but still return +the assembled TTL to the HTTP caller. GraphDB outage degrades the push +side without breaking the read side. -The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. +## Development with a local mbaigo clone -To build the software for one's own machine, -```go build -o modeler_imac```, where the ending is used to clarify for which platform the code is for. +Add both modules to the workspace `go.work` at the repository root: +``` +use ./mbaigo +use ./systems/kgrapher +``` -## Cross compiling/building -The following commands enable one to build for different platforms: +Or add a `replace` directive to `go.mod`: -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o kgrapher_rpi64``` -- Linux: GOOS=```GOARCH=amd64 go build -o kgrapher_amd64``` or ```GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o kgrapher_linux_amd64 .``` depending on the CPU (use ```uname -m``` to find out) +``` +require github.com/sdoque/mbaigo v0.x.x +replace github.com/sdoque/mbaigo => ../../mbaigo +``` -One can find a complete list of platform by typing *‌go tool dist list* at the command prompt +--- -If one wants to secure copy it to a Raspberry pi, -`scp kgrapher_rpi64 jan@192.168.1.195:demo/kgrapher/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *demo/kgrapher/* directory. +## Appendix A — Deploying GraphDB on a Raspberry Pi -## Deploying the asset -An easy way to deploy Ontotext GraphDB is with the use go Docker. +The KGrapher's `graphDBurl` is a URL like any other — the triple store +itself can run anywhere reachable. For a self-contained edge deployment +the Pi is a workable host. The walkthrough below was originally part of +the body of this README; it's preserved here as reference for new +deployments. -### 1. **Installing Docker** +### 1. Install Docker -#### Command: ```bash curl -sSL https://get.docker.com | sh ``` -- **`curl -sSL`**: This command uses `curl`, a tool to transfer data from or to a server. Here, it fetches the installation script for Docker. - - `-s` means "silent mode," so it won't show progress bars. - - `-S` makes sure to show errors if they occur. - - `-L` follows any redirects the URL might have. - -- **`https://get.docker.com`**: This is the URL of Docker’s official installation script, hosted by Docker. - -- **`| sh`**: This takes the output of the `curl` command (the script downloaded from Docker's server) and immediately runs it using `sh` (the shell command interpreter). - -In essence, this command fetches and runs Docker’s installation script, automatically installing Docker onto your Raspberry Pi. It ensures you're getting the most recent version of Docker. - -#### Security note: -This command is convenient, but it's generally a good practice to inspect scripts you download from the internet before running them. You can review it by downloading the script first without piping it into `sh`: + +- `curl -sSL` fetches the install script silently and follows redirects. +- `https://get.docker.com` is Docker's official install script. +- `| sh` pipes the script straight into the shell. + +To inspect the script before running: + ```bash curl -sSL https://get.docker.com -o get-docker.sh -``` -Then inspect the file with: -```bash cat get-docker.sh -``` -And run it manually with: -```bash sh get-docker.sh ``` -### 2. **Adding the User to the Docker Group** +### 2. Add the user to the docker group -#### Command: ```bash sudo usermod -aG docker pi ``` -- **`sudo`**: This command gives you superuser privileges, allowing you to execute actions that require administrative rights. - -- **`usermod -aG docker pi`**: - - **`usermod`**: This modifies a user’s account settings. - - **`-aG docker`**: This option appends the user to a group. In this case, it's appending the user to the `docker` group. - - `-a`: Append (don’t remove the user from other groups). - - `-G`: Specifies the group to which the user will be added (in this case, the `docker` group). - - **`pi`**: This is the username of the Raspberry Pi user account (if your account is named something different, replace `pi` with your username). +(Replace `pi` with your actual username if different.) Log out and back +in for the new group to take effect, then `docker` commands no longer +need `sudo`. -By adding the user to the `docker` group, you're giving the `pi` user permission to run Docker commands **without needing to use `sudo`** every time. After this, you'll need to log out and log back in (or reboot) for the changes to take effect. +### 3. Pull the GraphDB image -### 3. **Pulling the GraphDB Docker Image** - -#### Command: ```bash -docker pull ontotext/graphdb +docker pull ontotext/graphdb:10.7.4 ``` -- **`docker pull`**: This tells Docker to download (pull) an image from a Docker registry (by default, Docker Hub). - -- **`ontotext/graphdb`**: This is the name of the Docker image you're pulling. It's hosted on Docker Hub under the `ontotext` organization, and the specific image is `graphdb`. +Pin the version explicitly — `:latest` is convenient locally but unkind +when a fleet of Pis pulls a new image at different times. -This command downloads the `graphdb` Docker image to your Raspberry Pi, allowing you to run a container based on it. Docker images contain everything needed to run an application, including the application code, system libraries, and dependencies. +### 4. Run the container -### 4. **Running the GraphDB Docker Container** +Bridged networking (recommended for most setups): -#### Command: ```bash -docker run -d -p 7200:7200 --name graphdb -t ontotext/graphdb:10.7.4 -docker run -d --network host --name graphdb ontotext/graphdb:10.7.4 +docker run -d -p 7200:7200 --name graphdb ontotext/graphdb:10.7.4 ``` -- **`docker run`**: This starts a new container from a Docker image. - -- **`-d`**: This tells Docker to run the container in **detached mode**, meaning the container will run in the background and won't tie up your terminal session. - -- **`-p 7200:7200`**: This option maps port 7200 on your Raspberry Pi (host) to port 7200 inside the Docker container. GraphDB uses port 7200 for its web interface, so this allows you to access GraphDB on your Raspberry Pi through `http://:7200`. +Host networking (only if you need the container to bind on the host's +IP directly — e.g., for some service-discovery setups): -- **`--name graphdb`**: This gives the container a name (`graphdb`). This is useful for managing or referring to the container later (e.g., stopping it, viewing logs, etc.). +```bash +docker run -d --network host --name graphdb ontotext/graphdb:10.7.4 +``` -- **`ontotext/graphdb`**: This specifies the image to use to create the container. Docker will use the image you pulled earlier. +Choose **one** of the two. The bridged form is the default; use the host +form only when you have a specific reason. +### 5. Stop and remove -To shut the running instance, type ```docker stop graphdb``` and then ```docker rm graphdb``` . \ No newline at end of file +```bash +docker stop graphdb +docker rm graphdb +``` diff --git a/kgrapher/go.mod b/kgrapher/go.mod index c1e1379..e66f6f5 100644 --- a/kgrapher/go.mod +++ b/kgrapher/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/kgrapher -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/leveler/go.mod b/leveler/go.mod index 6293206..aeb33ef 100644 --- a/leveler/go.mod +++ b/leveler/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/leveler -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/maitreD/go.mod b/maitreD/go.mod index 84a58f6..78a48d1 100644 --- a/maitreD/go.mod +++ b/maitreD/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/maitreD -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/messenger/go.mod b/messenger/go.mod index f648288..59eff22 100644 --- a/messenger/go.mod +++ b/messenger/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/messenger -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/meteorologue/go.mod b/meteorologue/go.mod index b4e901f..f799987 100644 --- a/meteorologue/go.mod +++ b/meteorologue/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/meteorologue -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/modboss/go.mod b/modboss/go.mod index 9458af3..4510a8c 100644 --- a/modboss/go.mod +++ b/modboss/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/modboss -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/modeler/README.md b/modeler/README.md index 2794e67..0d782f7 100644 --- a/modeler/README.md +++ b/modeler/README.md @@ -2,20 +2,54 @@ ## Purpose -The Modeler system assembles a complete **SysML v2** structural and behavioural model of a local cloud — a distributed system of systems — by collecting the individual model fragment of each registered system and merging them into a single, coherent package. +The *Modeler* assembles a complete **SysML v2** structural and behavioural +model of an Arrowhead local cloud — a distributed system of systems — by +collecting the per-system model fragment from each registered system and +merging them into a single, coherent SysML v2 package. The result is what +SysML v2 tooling reads when it wants to visualise, validate, or reason +about the cloud as a whole. The output covers: -- **mAF library** — a SysML v2 package that defines the Arrowhead vocabulary (`ArrowheadSystem`, `ServiceRegistrar`, `Orchestrator`, `CertificateAuthority`, `UnitAsset`, `Host`, `LocalCloud`, plus the abstract actions `GetState`/`SetState`/`Compute`). Emitted inline at the top of every assembled cloud package so the output is self-contained. The library source lives in [`mAF.sysml`](mAF.sysml) and is embedded at compile time via `go:embed`. -- **Block Definition Diagram (BDD)** — concrete part defs for each system and unit asset, all specialised from mAF abstractions (`:> ArrowheadSystem`, `:> UnitAsset`, etc.) plus a `Def` specialisation of `LocalCloud` that lists the hosts and systems in the deployment -- **Internal Block Diagram (IBD)** — the instantiated parts with their host metadata and live service connections at the time of the request -- **Behaviour Definitions** — per-asset action sequences derived from each unit asset's consumed services, when those cervices carry a `Mode` ("get" or "set") -The model is generated on demand by issuing an HTTP GET to the `cloudmodel` service. -It is expressed in [SysML v2 textual notation](https://www.omg.org/spec/SysML/2.0) and returned as plain text. +- **mAF library** — a SysML v2 package that defines the Arrowhead + vocabulary (`ArrowheadSystem`, `ServiceRegistrar`, `Orchestrator`, + `CertificateAuthority`, `UnitAsset`, `Host`, `LocalCloud`, plus the + abstract actions `GetState` / `SetState` / `Compute`). Emitted inline at + the top of every assembled cloud package so the output is + self-contained. The library source lives in [`mAF.sysml`](mAF.sysml) + and is embedded at compile time via `go:embed`. +- **Block Definition Diagram (BDD)** — concrete part defs for each system + and unit asset, all specialised from mAF abstractions + (`:> ArrowheadSystem`, `:> UnitAsset`, etc.) plus a `Def` + specialisation of `LocalCloud` listing the hosts and systems. +- **Internal Block Diagram (IBD)** — the instantiated parts with host + metadata and live service connections resolved at request time. +- **Behaviour Definitions** — per-asset action sequences derived from + each unit asset's consumed services when those cervices carry a + `Mode` (`"get"` or `"set"`). + +The model is generated **on demand** by issuing an HTTP GET to the +`cloudmodel` service. It is expressed in [SysML v2 textual +notation](https://www.omg.org/spec/SysML/2.0) and returned as plain text. + +## Architecture + +The Modeler is an **aggregator system**, the SysML v2 sibling of the +[kgrapher](../kgrapher/) (which plays the same role for OWL/RDF). Both +discover every system via the Service Registrar, fetch each system's +per-system meta-view, deduplicate and merge, hand back the combined +result. The Modeler does no per-system model generation; it only +orchestrates collection and merging. + +The Modeler is **demand-driven** — there is no background timer. Each +GET on `/cloudmodel` triggers a fresh collect + merge. Consumers that +want a current view ask for one. ## Relationship to AFO -The [AFO OWL ontology](https://github.com/sdoque/kgrapher) and the mAF SysML library describe the same domain — an Arrowhead local cloud — from two complementary angles: +The [AFO OWL ontology](https://github.com/sdoque/kgrapher) and the mAF +SysML library describe the same domain — an Arrowhead local cloud — from +two complementary angles: | Concern | AFO (OWL) | mAF (SysML v2) | |---------------------------------|-----------------|----------------------------------| @@ -26,37 +60,141 @@ The [AFO OWL ontology](https://github.com/sdoque/kgrapher) and the mAF SysML lib | Behaviour (action sequences) | No | First-class (`action def`) | | Connections between parts | Derivable | First-class (`connect`) | -The kgrapher emits AFO instances; the modeler emits mAF-specialised SysML v2. The two outputs are views of the same runtime topology, each suited to different tooling. +The kgrapher emits AFO instances; the modeler emits mAF-specialised +SysML v2. The two outputs are views of the same runtime topology, each +suited to different tooling. -## How it works +## How it fits in the cloud ```mermaid sequenceDiagram + autonumber actor User - participant Modeler - participant ServiceRegistrar - participant System + participant M as Modeler + participant SR as ServiceRegistrar + participant S as System - User->>Modeler: GET /modeler/assembler/cloudmodel - Modeler->>ServiceRegistrar: GET /serviceregistrar/registry/syslist - ServiceRegistrar-->>Modeler: SystemRecordList (list of base URLs) + User->>M: GET /modeler/assembler/cloudmodel + M->>SR: GET /serviceregistrar/registry/syslist + SR-->>M: SystemRecordList (list of base URLs) - loop for each system in the list - Modeler->>System: GET //smodel - System-->>Modeler: SysML v2 package (port defs, part defs, IBD part, behaviour defs) + loop for each system + M->>S: GET //smodel + S-->>M: SysML v2 fragment (port defs, part defs, IBD part, behaviour defs) end - Modeler->>Modeler: merge packages
(deduplicate port defs and abstract action defs,
collect block defs, IBD parts, behaviour defs) - Modeler-->>User: SysML v2 package 'localCloud' { … } + M->>M: merge (dedupe port + abstract action defs,
collect block defs, IBD parts, behaviour defs) + M-->>User: SysML v2 package 'localCloud' { … } ``` -Each system's `/smodel` endpoint (provided by the `mbaigo` framework) generates a SysML v2 fragment with: -- **port defs** — one per unique service definition (provided or consumed) -- **part defs** — one for the system (named `System`, specialised from an mAF type — `ServiceRegistrar`/`Orchestrator`/`CertificateAuthority` for core systems, `ArrowheadSystem` for domain systems) and one per unit asset (named `_UnitAsset`, specialised from `UnitAsset`). Asset type names are qualified by the system to prevent collisions when two systems happen to use the same asset name. -- **IBD part** — the instantiated system with its host metadata, provided service URLs as comments, and `@connect` annotations for any already-resolved service providers -- **behaviour defs** — one `action def` per unit asset whose cervices carry a `Mode`, with a linear `first X then Y;` sequence referencing mAF's abstract `GetState`/`SetState`/`Compute` action defs +Each system's `/smodel` endpoint (provided by the `mbaigo` framework) +generates a SysML v2 fragment with: + +- **port defs** — one per unique service definition (provided or consumed). +- **part defs** — one for the system (named `System`, specialised + from an mAF type — `ServiceRegistrar` / `Orchestrator` / + `CertificateAuthority` for core systems, `ArrowheadSystem` for domain + systems) and one per unit asset (named `_UnitAsset`, + specialised from `UnitAsset`). Asset type names are qualified by the + system to prevent collisions when two systems happen to use the same + asset name. +- **IBD part** — the instantiated system with its host metadata, + provided service URLs as comments, and `@connect` annotations for any + already-resolved service providers. +- **behaviour defs** — one `action def` per unit asset whose cervices + carry a `Mode`, with a linear `first X then Y;` sequence referencing + mAF's abstract `GetState` / `SetState` / `Compute` action defs. + +The Modeler deduplicates `port def` declarations, emits the mAF library +inline at the top of the output, wraps the per-system defs in a +`package '' { import mAF::*; ... }`, emits a concrete +`Def :> LocalCloud` type listing hosts and systems, and +produces a `LocalCloud` IBD instance that holds the actual host/system +attribute values (via `redefines`) plus **formal `connect` statements** +resolved from each consumer's `@connect` URL against the providers seen +in the same assembly pass. + +## Services + +### Provided + +| Service definition | Subpath | Methods | Description | +|--------------------|---------|---------|-------------| +| `cloudmodel` | `cloudmodel` | `GET` | Assembles the cloud's SysML v2 model from every registered system and returns the merged package as `text/plain` | + +### Consumed (via Arrowhead orchestration) + +| Service definition | Used for | +|--------------------|----------| +| Pulled from every registered system via `//smodel` | Per-system SysML v2 fragments to merge | -The Modeler deduplicates `port def` declarations, emits the mAF library inline at the top of the output, wraps the per-system defs in a `package '' { import mAF::*; ... }`, emits a concrete `Def :> LocalCloud` type listing the hosts and systems, and produces a `LocalCloud` IBD instance that holds the actual host/system attribute values (via `redefines`) plus **formal `connect` statements** resolved from each consumer's `@connect` URL against the providers seen in the same assembly pass. +## Configuration + +```json +{ + "systemname": "modeler", + "unit_assets": [ + { + "name": "assembler", + "details": { "Type": ["Interactive"] }, + "services": [ + { + "definition": "cloudmodel", + "subpath": "cloudmodel", + "details": { "Format": ["SysML v2"] }, + "registrationPeriod": 61 + } + ], + "traits": [ + { "cloudName": "myLocalCloud" } + ] + } + ], + "protocolsNports": { "coap": 0, "http": 20106, "https": 0 }, + "coreSystems": [ /* serviceregistrar, orchestrator, ca, maitreD */ ] +} +``` + +### Trait reference + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `cloudName` | string | `"localCloud"` | Name used for the merged SysML v2 package and for the concrete `Def :> LocalCloud` type | + +## Behaviour generation + +A behaviour block is emitted for a unit asset when at least one of its +consumed services (cervices) carries a `Mode` field set to `"get"` or +`"set"`. The sequence is always linear: + +1. all `"get"` cervices (sorted alphabetically) — each becomes a + `GetState` action. +2. a `compute` step (`Compute`) — inserted only when both gets and sets + are present. +3. all `"set"` cervices (sorted alphabetically) — each becomes a + `SetState` action. + +Consecutive steps are linked with `first X then Y;` pairs. + +## Extending the mAF library + +The [`mAF.sysml`](mAF.sysml) file is the source of truth for the +Arrowhead vocabulary in SysML v2 terms. If you add a new core system +category, a new kind of action, or want to tighten cardinality +constraints on `LocalCloud`, edit that file directly — the change takes +effect on the next `go build` because the library is embedded at compile +time. No Go code needs to be touched unless you also change how concrete +systems specialise from it (that logic lives in +`mbaigo/usecases/smodeling.go`). + +Guidelines: + +- Keep mAF **abstract**. Concrete part defs belong in the generated + output, not in the library. +- Keep mAF **aligned with AFO**. If you add a class to AFO, add the + equivalent abstract part def to mAF (or explain why it doesn't map). +- mAF expresses what SysML v2 can natively express. Reasoning that + belongs in OWL (property chains, class intersections) stays in AFO. ## Output example @@ -100,13 +238,10 @@ package 'AlphaCloud' { private import mAF::*; // ── Port Definitions ───────────────────────────────────────────────────── - // Port def names are capitalised so they differ from the port usage - // names below. (SysML v2 requires def and usage to use different - // identifiers.) port def 'Temperature'; port def 'Rotation'; port def 'Setpoint'; - ... + // … // ── Block Definitions (BDD) ────────────────────────────────────────────── part def 'thermostatSystem' :> ArrowheadSystem { @@ -125,15 +260,14 @@ package 'AlphaCloud' { perform action behave : 'thermostat_controller_1Behavior'; } - part def 'serviceregistrarSystem' :> ServiceRegistrar { ... } - part def 'orchestratorSystem' :> Orchestrator { ... } - ... + part def 'serviceregistrarSystem' :> ServiceRegistrar { /* … */ } + part def 'orchestratorSystem' :> Orchestrator { /* … */ } part def 'AlphaCloudDef' :> LocalCloud { part canbus : Host; part thermostat : 'thermostatSystem'; part ds18b20 : 'ds18b20System'; - ... + // … } // ── Behaviour Definitions ──────────────────────────────────────────────── @@ -145,7 +279,6 @@ package 'AlphaCloud' { first 'get_temperature' then compute; first compute then 'set_rotation'; } - ... // ── Internal Block Diagram (IBD) ───────────────────────────────────────── part 'AlphaCloud' : 'AlphaCloudDef' { @@ -161,7 +294,6 @@ package 'AlphaCloud' { attribute redefines httpPort : Integer = 20152; // provides: http://192.168.1.10:20152/thermostat/controller_1/setpoint } - ... // ── Connections ────────────────────────────────────────────────────── connect thermostat.controller_1.temperature to ds18b20.'28-00000f030344'.temperature; @@ -169,72 +301,49 @@ package 'AlphaCloud' { } ``` -## Behaviour generation - -A behaviour block is emitted for a unit asset when at least one of its consumed services (cervices) carries a `Mode` field set to `"get"` or `"set"`. The sequence is always linear: - -1. all `"get"` cervices (sorted alphabetically) — each becomes a `GetState` action -2. a `compute` step (`Compute`) — inserted only when both gets and sets are present -3. all `"set"` cervices (sorted alphabetically) — each becomes a `SetState` action +## Building and running -Consecutive steps are linked with `first X then Y;` pairs. - -## Extending the mAF library - -The [`mAF.sysml`](mAF.sysml) file is the source of truth for the Arrowhead vocabulary in SysML v2 terms. If you add a new core system category, a new kind of action, or want to tighten cardinality constraints on `LocalCloud`, edit that file directly — the change takes effect on the next `go build` because the library is embedded at compile time. No Go code needs to be touched unless you also change how concrete systems specialise from it (that logic lives in `mbaigo/usecases/smodeling.go`). - -Guidelines: -- Keep mAF **abstract**. Concrete part defs belong in the generated output, not in the library. -- Keep mAF **aligned with AFO**. If you add a class to AFO, add the equivalent abstract part def to mAF (or explain why it doesn't map). -- mAF expresses what SysML v2 can natively express. Reasoning that belongs in OWL (property chains, class intersections) stays in AFO. +```bash +# Run from source (development) +go run . -## Configuration +# Build a binary for the current machine +go build -o modeler_amac . -The only configurable trait is the name of the merged package: +# Cross-compile for a 64-bit Raspberry Pi +GOOS=linux GOARCH=arm64 go build -o modeler_rpi64 . -```json -"traits": [{ "cloudName": "myLocalCloud" }] +# Deploy +scp modeler_rpi64 jan@:oslo/modeler/ ``` -If omitted, the package is named `localCloud`. - -## Compiling +Run the binary from **inside its own directory** so it can find (or +auto-generate) `systemconfig.json`. A full list of supported platforms: +`go tool dist list`. -Initialise the module once (already done if `go.mod` is present): +## Startup order -```bash -go mod init github.com/sdoque/systems/modeler -go mod tidy ``` - -Run directly from the system's directory: - -```bash -go run . +Arrowhead core systems → Modeler → any consumer ``` -> It is **important** to start the program from within its own directory because it looks for `systemconfig.json` there. If the file is absent, a default one is generated and the program stops so the file can be reviewed and adjusted before the next start. +The Modeler is **demand-driven and stateless** — it pulls from the +registrar on each request, so application systems can join after it's +already running. The first `/cloudmodel` request after a system joins +will include that system's fragment automatically. -The address of the running web server is printed at startup and can be opened in any browser. +## Development with a local mbaigo clone -To build a binary for the local machine: +Add both modules to the workspace `go.work` at the repository root: -```bash -go build -o modeler_local +``` +use ./mbaigo +use ./systems/modeler ``` -## Cross-compiling - -| Target | Command | -|--------|---------| -| Raspberry Pi 64-bit | `GOOS=linux GOARCH=arm64 go build -o modeler_rpi64` | -| Linux x86-64 | `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o modeler_linux_amd64` | -| macOS Apple Silicon | `GOOS=darwin GOARCH=arm64 go build -o modeler_mac_arm64` | - -A full list of supported platforms: `go tool dist list` - -To copy the binary to a remote host: +Or add a `replace` directive to `go.mod`: -```bash -scp modeler_rpi64 user@192.168.1.x:~/demo/modeler/ +``` +require github.com/sdoque/mbaigo v0.x.x +replace github.com/sdoque/mbaigo => ../../mbaigo ``` diff --git a/modeler/go.mod b/modeler/go.mod index 64bd16d..0bb22b3 100644 --- a/modeler/go.mod +++ b/modeler/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/modeler -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/nurse/go.mod b/nurse/go.mod index 6f3d4f2..d6547a6 100644 --- a/nurse/go.mod +++ b/nurse/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/nurse -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/orchestrator/go.mod b/orchestrator/go.mod index 01f4936..5212599 100644 --- a/orchestrator/go.mod +++ b/orchestrator/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/orchestrator -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/parallax/go.mod b/parallax/go.mod index 04adc72..218ca62 100644 --- a/parallax/go.mod +++ b/parallax/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/parallax -go 1.26.3 +go 1.26.4 require ( github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/photographer/go.mod b/photographer/go.mod index 12b2522..6ea23e8 100644 --- a/photographer/go.mod +++ b/photographer/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/photographer -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/recognizer/go.mod b/recognizer/go.mod index 5932250..ac39c56 100644 --- a/recognizer/go.mod +++ b/recognizer/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/recognizer -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/revolutionary/go.mod b/revolutionary/go.mod index d607166..76fe4ce 100644 --- a/revolutionary/go.mod +++ b/revolutionary/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/revolutionary -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/sailor/go.mod b/sailor/go.mod index 75b7df2..7b92b66 100644 --- a/sailor/go.mod +++ b/sailor/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/sailor -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/sapper/WorkOrder_Repair_135_Dates.ttl b/sapper/WorkOrder_Repair_135_Dates.ttl new file mode 100644 index 0000000..214e8a2 --- /dev/null +++ b/sapper/WorkOrder_Repair_135_Dates.ttl @@ -0,0 +1,235 @@ +# Repair of equipment connected to WorkOrder 400000135 + +PREFIX : +PREFIX ex: +PREFIX dcterms: + +PREFIX step: + +PREFIX workorder: +PREFIX wra: + +PREFIX dpa: +PREFIX ahr: +PREFIX aa: +PREFIX actualactivity: +PREFIX eff: +PREFIX de: +PREFIX ber: +PREFIX pva: +PREFIX woa: + +PREFIX xsd: + +DELETE { + GRAPH { + ?workOrder ex:status ?oldStatus . + ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate . + ?workOrder dcterms:modified ?oldModified . + } +} +INSERT { + GRAPH { + # Update WorkOrder status/completion metadata + ?workOrder + ex:status ?newStatus ; + ex:technicalCompletionDate ?technicalCompletionDateTime ; + dcterms:modified ?modifiedDateTime . + + # Relationship between WorkOrder and ActualActivity + ?directedPlannedActivity + a step:DirectedPlannedActivity ; + dpa:Directive ?workOrder . + + ?actualActivity + a step:ActualActivity ; + actualactivity:ActualStartDate [ + a step:DateTimeString, step:String ; + :value ?activityStartDateTime + ] ; + actualactivity:ActualEndDate [ + a step:DateTimeString, step:String ; + :value ?activityEndDateTime + ] . + + ?activityHappeningRelationship + a step:ActivityHappeningRelationship ; + ahr:Relating ?directedPlannedActivity ; + ahr:Related ?actualActivity . + + # Activity assignment: repaired currently fitted part + ?activityAssignmentRepairedPart + a step:ActivityAssignment ; + aa:AssignedTo ?individualPart ; + aa:AssignedActivity ?actualActivity . + + # Activity assignment: dated effectivity of the repair work itself + ?activityAssignmentRepairActivityEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?repairActivityEffectivity ; + aa:AssignedActivity ?actualActivity . + + # WorkOrderAssignment to the blueprint / breakdown item + ?workOrderAssignment + a step:WorkOrderAssignment ; + woa:AssignedTo ?breakdownItem ; + woa:AssignedWorkOrder ?workOrder . + } + + GRAPH { + # Dated effectivity for the repair activity itself + # This uses the same activity start/end values as the ActualActivity. + ?repairActivityEffectivity + a step:EffectivityAssignment ; + eff:AssignedTo ?actualActivity ; + eff:AssignedEffectivity ?repairActivityDatedEffectivity ; + eff:EffectivityIndication step:True . + + ?repairActivityDatedEffectivity + a step:DatedEffectivity ; + de:StartDefinition ?repairActivityStartDateTimeString ; + de:EndDefinition ?repairActivityEndDateTimeString . + + ?repairActivityStartDateTimeString + a step:DateTimeString, + step:String ; + :value ?activityStartDateTime . + + ?repairActivityEndDateTimeString + a step:DateTimeString, + step:String ; + :value ?activityEndDateTime . + } +} +WHERE { + # Parameters for this test + BIND(IRI("https://sinetiq.se/sap/MaintenanceOrder/400000135") AS ?workOrder) + BIND(IRI("https://sinetiq.se/sap/MaintenanceOrder/WorkOrderAssignment_400000135") AS ?workOrderAssignment) + + # Existing currently-installed IndividualPartView that was repaired + BIND(IRI("https://arrowheadweb.org/data/SK_000012") AS ?individualPart) + + # WorkOrder metadata + BIND("TECO" AS ?newStatus) + BIND("2024-06-01T12:00:00"^^xsd:dateTime AS ?technicalCompletionDateTime) + BIND("2024-06-01T12:00:00"^^xsd:dateTime AS ?modifiedDateTime) + + # Repair activity interval + BIND("2024-06-01T08:00:00"^^xsd:dateTime AS ?activityStartDateTime) + BIND("2024-06-01T12:00:00"^^xsd:dateTime AS ?activityEndDateTime) + + # Extract WorkOrder number for cleaner generated URIs + BIND(REPLACE(STR(?workOrder), "^.*/", "") AS ?workOrderId) + + # Existing WorkOrder metadata to replace + OPTIONAL { + GRAPH { + ?workOrder ex:status ?oldStatus . + } + } + + OPTIONAL { + GRAPH { + ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate . + } + } + + OPTIONAL { + GRAPH { + ?workOrder dcterms:modified ?oldModified . + } + } + + # Repaired IndividualPartView must exist in graph/parts + GRAPH { + ?individualPart a step:IndividualPartView . + } + + # Existing association lookups + GRAPH { + ?partRealization + pva:AssociatedIndividualPart ?individualPart ; + pva:AssociatedPart ?part . + + ?breakdownRealization + ber:RealizedAs ?individualPart ; + ber:BreakdownItem ?breakdownItem . + } + + # Validate that this WorkOrder's WorkRequest is assigned to the same breakdown item + GRAPH { + ?workOrder + a step:WorkOrder ; + workorder:InResponseTo ?workRequest . + + ?workRequestAssignment + a step:WorkRequestAssignment ; + wra:AssignedWorkRequest ?workRequest ; + wra:AssignedTo ?breakdownItem . + } + + # Validate that the repaired IndividualPartView is the currently active fitted part + GRAPH { + ?effCurrent + eff:AssignedTo ?breakdownRealization ; + eff:AssignedEffectivity ?currentDatedEffectivity . + + # Current fitted part = has active effectivity, meaning no EndDefinition. + FILTER NOT EXISTS { + ?currentDatedEffectivity de:EndDefinition ?existingEndDefinition . + } + } + + # URI generation: activity/work-order side + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DirectedPlannedActivity_", + ?workOrderId + )) AS ?directedPlannedActivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActivityHappeningRelationship_", + ?workOrderId + )) AS ?activityHappeningRelationship) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActualActivity_", + ?workOrderId + )) AS ?actualActivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", + ?workOrderId, + "_RepairedPart" + )) AS ?activityAssignmentRepairedPart) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", + ?workOrderId, + "_RepairActivityEffectivity" + )) AS ?activityAssignmentRepairActivityEffectivity) + + # URI generation: repair activity effectivity + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/EffectivityAssignment_", + ?workOrderId, + "_RepairActivity" + )) AS ?repairActivityEffectivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DatedEffectivity_", + ?workOrderId, + "_RepairActivity" + )) AS ?repairActivityDatedEffectivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_Start_", + ?workOrderId, + "_RepairActivity" + )) AS ?repairActivityStartDateTimeString) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_End_", + ?workOrderId, + "_RepairActivity" + )) AS ?repairActivityEndDateTimeString) +} \ No newline at end of file diff --git a/sapper/WorkOrder_Replace_134_Dates.ttl b/sapper/WorkOrder_Replace_134_Dates.ttl new file mode 100644 index 0000000..c2c0195 --- /dev/null +++ b/sapper/WorkOrder_Replace_134_Dates.ttl @@ -0,0 +1,340 @@ +# Replacement of equipment connected to WorkOrder 400000134 + +PREFIX : +PREFIX ex: +PREFIX dcterms: +PREFIX ido-ext: + +PREFIX step: + +PREFIX workorder: +PREFIX wra: + +PREFIX dpa: +PREFIX ahr: +PREFIX aa: +PREFIX actualactivity: +PREFIX eff: +PREFIX de: +PREFIX ber: +PREFIX pva: +PREFIX woa: + +PREFIX xsd: + +DELETE { + GRAPH { + ?workOrder ex:status ?oldStatus . + ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate . + ?workOrder dcterms:modified ?oldModified . + } +} +INSERT { + GRAPH { + # Update WorkOrder status/completion metadata + ?workOrder + ex:status ?newStatus ; + ex:technicalCompletionDate ?technicalCompletionDateTime ; + dcterms:modified ?modifiedDateTime . + + # Relationship between WorkOrder and ActualActivity + ?directedPlannedActivity + a step:DirectedPlannedActivity ; + dpa:Directive ?workOrder . + + ?actualActivity + a step:ActualActivity ; + actualactivity:ActualStartDate [ + a step:DateTimeString, step:String ; + :value ?activityStartDateTime + ] ; + actualactivity:ActualEndDate [ + a step:DateTimeString, step:String ; + :value ?activityEndDateTime + ] . + + ?activityHappeningRelationship + a step:ActivityHappeningRelationship ; + ahr:Relating ?directedPlannedActivity ; + ahr:Related ?actualActivity . + + # Activity assignment: outgoing effectivity closed by this activity + ?activityAssignmentOutgoingEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?effOld ; + aa:AssignedActivity ?actualActivity . + + # Activity assignment: incoming effectivity opened by this activity + ?activityAssignmentIncomingEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?effNew ; + aa:AssignedActivity ?actualActivity . + + # Activity assignment: dated effectivity of the replacement work itself + ?activityAssignmentReplacementActivityEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?replacementActivityEffectivity ; + aa:AssignedActivity ?actualActivity . + + # WorkOrderAssignment to the blueprint / breakdown item + ?workOrderAssignment + a step:WorkOrderAssignment ; + woa:AssignedTo ?breakdownItem ; + woa:AssignedWorkOrder ?workOrder . + } + + GRAPH { + # New IndividualPartView resource + ?newIndividualPart + a step:IndividualPartView, + ido-ext:IndividualPart . + } + + GRAPH { + # New blueprint/equipment-to-individual-part realization + ?newRealization + a :AssociationObject, + step:BreakdownElementRealization ; + ber:RealizedAs ?newIndividualPart ; + ber:BreakdownItem ?breakdownItem . + + # Link new IndividualPartView to existing PartView definition + ?newPvIpvAssociation + a :AssociationObject, + step:PartViewToIndividualPartViewAssociation ; + pva:AssociatedIndividualPart ?newIndividualPart ; + pva:AssociatedPart ?part . + } + + GRAPH { + # Close old fitted-part effectivity + ?effOldEffectivity + de:EndDefinition ?oldEndDateTimeString . + + ?oldEndDateTimeString + a step:DateTimeString, + step:String ; + :value ?activityEndDateTime . + + # Create new fitted-part effectivity + # This intentionally only has StartDefinition. + # No EndDefinition here, because this is what makes the replacement part the current fitted part. + ?effNew + a step:EffectivityAssignment ; + eff:AssignedTo ?newRealization ; + eff:AssignedEffectivity ?newDatedEffectivity ; + eff:EffectivityIndication step:True . + + ?newDatedEffectivity + a step:DatedEffectivity ; + de:StartDefinition ?newStartDateTimeString . + + ?newStartDateTimeString + a step:DateTimeString, + step:String ; + :value ?activityEndDateTime . + + # Dated effectivity for the replacement activity itself + # This uses the same activity start/end values as the ActualActivity. + ?replacementActivityEffectivity + a step:EffectivityAssignment ; + eff:AssignedTo ?actualActivity ; + eff:AssignedEffectivity ?replacementActivityDatedEffectivity ; + eff:EffectivityIndication step:True . + + ?replacementActivityDatedEffectivity + a step:DatedEffectivity ; + de:StartDefinition ?replacementActivityStartDateTimeString ; + de:EndDefinition ?replacementActivityEndDateTimeString . + + ?replacementActivityStartDateTimeString + a step:DateTimeString, + step:String ; + :value ?activityStartDateTime . + + ?replacementActivityEndDateTimeString + a step:DateTimeString, + step:String ; + :value ?activityEndDateTime . + } + + GRAPH { + # Implementation relation between FunctionalLocation / BreakdownElementView + # and the new currently fitted IndividualPart + ?newIndividualPart + ido-ext:implements ?breakdownItem . + + # Implementation relation between Part and new IndividualPart + ?newIndividualPart + ido-ext:implements ?part . + } +} +WHERE { + # Parameters for this test + BIND(IRI("https://sinetiq.se/sap/MaintenanceOrder/400000134") AS ?workOrder) + BIND(IRI("https://sinetiq.se/sap/MaintenanceOrder/WorkOrderAssignment_400000134") AS ?workOrderAssignment) + + # Existing currently-installed IndividualPartView to replace + BIND(IRI("https://arrowheadweb.org/data/SK_002285") AS ?individualPart) # The example SK_002285 is current part at functional location 827-PV2708-200 + + # New test IndividualPartView + BIND(IRI("https://arrowheadweb.org/data/SK_002288") AS ?newIndividualPart) # The example SK_002288 is the fitting replacement part for SK_002285 + + # WorkOrder metadata + BIND("TECO" AS ?newStatus) + BIND("2024-06-01T12:00:00"^^xsd:dateTime AS ?technicalCompletionDateTime) + BIND("2024-06-01T12:00:00"^^xsd:dateTime AS ?modifiedDateTime) + + # Replacement activity interval + BIND("2024-06-01T08:00:00"^^xsd:dateTime AS ?activityStartDateTime) + BIND("2024-06-01T12:00:00"^^xsd:dateTime AS ?activityEndDateTime) + + # Extract WorkOrder number for cleaner generated URIs + BIND(REPLACE(STR(?workOrder), "^.*/", "") AS ?workOrderId) + + # Existing WorkOrder metadata to replace + OPTIONAL { + GRAPH { + ?workOrder ex:status ?oldStatus . + } + } + + OPTIONAL { + GRAPH { + ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate . + } + } + + OPTIONAL { + GRAPH { + ?workOrder dcterms:modified ?oldModified . + } + } + + # Old IndividualPartView must exist in graph/parts + GRAPH { + ?individualPart a step:IndividualPartView . + } + + # Existing association lookups + GRAPH { + ?partRealization + pva:AssociatedIndividualPart ?individualPart ; + pva:AssociatedPart ?part . + + ?breakdownRealization + ber:RealizedAs ?individualPart ; + ber:BreakdownItem ?breakdownItem . + } + + # Validate that this WorkOrder's WorkRequest is assigned to the same breakdown item + GRAPH { + ?workOrder + a step:WorkOrder ; + workorder:InResponseTo ?workRequest . + + ?workRequestAssignment + a step:WorkRequestAssignment ; + wra:AssignedWorkRequest ?workRequest ; + wra:AssignedTo ?breakdownItem . + } + + # Existing active/old effectivity lookup + GRAPH { + ?effOld + eff:AssignedTo ?breakdownRealization ; + eff:AssignedEffectivity ?effOldEffectivity . + + # Prevent adding another EndDefinition if this effectivity is already closed. + FILTER NOT EXISTS { + ?effOldEffectivity de:EndDefinition ?existingEndDefinition . + } + } + + # URI generation: activity/work-order side + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DirectedPlannedActivity_", + ?workOrderId + )) AS ?directedPlannedActivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActivityHappeningRelationship_", + ?workOrderId + )) AS ?activityHappeningRelationship) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActualActivity_", + ?workOrderId + )) AS ?actualActivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", + ?workOrderId, + "_OutgoingEffectivity" + )) AS ?activityAssignmentOutgoingEffectivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", + ?workOrderId, + "_IncomingEffectivity" + )) AS ?activityAssignmentIncomingEffectivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", + ?workOrderId, + "_ReplacementActivityEffectivity" + )) AS ?activityAssignmentReplacementActivityEffectivity) + + # URI generation: new associations + BIND(IRI(CONCAT(STR(?breakdownRealization), "-replacement")) AS ?newRealization) + BIND(IRI(CONCAT(STR(?partRealization), "-replacement")) AS ?newPvIpvAssociation) + + # URI generation: effectivity naming based on realization IDs + BIND(REPLACE(STR(?breakdownRealization), "^.*[/#]", "") AS ?oldRealizationId) + BIND(REPLACE(STR(?newRealization), "^.*[/#]", "") AS ?newRealizationId) + + BIND(IRI(CONCAT( + "https://arrowheadweb.org/data/DateTimeString_End_", + ENCODE_FOR_URI(?oldRealizationId) + )) AS ?oldEndDateTimeString) + + BIND(IRI(CONCAT( + "https://arrowheadweb.org/data/EffectivityAssignment_", + ENCODE_FOR_URI(?newRealizationId) + )) AS ?effNew) + + BIND(IRI(CONCAT( + "https://arrowheadweb.org/data/DatedEffectivity_", + ENCODE_FOR_URI(?newRealizationId) + )) AS ?newDatedEffectivity) + + BIND(IRI(CONCAT( + "https://arrowheadweb.org/data/DateTimeString_", + ENCODE_FOR_URI(?newRealizationId) + )) AS ?newStartDateTimeString) + + # URI generation: replacement activity effectivity + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/EffectivityAssignment_", + ?workOrderId, + "_ReplacementActivity" + )) AS ?replacementActivityEffectivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DatedEffectivity_", + ?workOrderId, + "_ReplacementActivity" + )) AS ?replacementActivityDatedEffectivity) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_Start_", + ?workOrderId, + "_ReplacementActivity" + )) AS ?replacementActivityStartDateTimeString) + + BIND(IRI(CONCAT( + "https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_End_", + ?workOrderId, + "_ReplacementActivity" + )) AS ?replacementActivityEndDateTimeString) +} \ No newline at end of file diff --git a/sapper/go.mod b/sapper/go.mod index a878ca8..c2402e2 100644 --- a/sapper/go.mod +++ b/sapper/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/sapper -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/sapper/models.go b/sapper/models.go index 3411b5e..837df38 100644 --- a/sapper/models.go +++ b/sapper/models.go @@ -76,8 +76,22 @@ type Order struct { CreatedAt time.Time ReleasedAt time.Time // zero until enrichAndRelease is called Request OrderRequest - SuggestedEnrichment string // template the firefighting UI prefills when this order is selected; defaults to the seals/gaskets example + CurrentPartIRI string // IRI of the part currently fitted at the FL, resolved from GraphDB at order-create time; empty if unresolved + SuggestedEnrichment string // template the firefighting UI prefills when this order is selected Enrichment json.RawMessage // planner's actual submission; nil until released + ParsedEnrichment *Enrichment // structured view of Enrichment; nil if parse failed or not yet released +} + +// Enrichment is the structured form of the planner's firefighting submission. +// The planner's textarea contains JSON with this shape; the Sapper parses it on +// release and uses it to drive which TECO SPARQL template runs (Repair vs Replace). +type Enrichment struct { + Decision string `json:"decision"` // "repair" or "replace" + CurrentPart string `json:"currentPart"` // IRI of the currently fitted part (pre-filled, planner can override) + NewPart string `json:"newPart,omitempty"` // IRI of the replacement part (required when decision="replace") + ActivityStart string `json:"activityStart"` // ISO 8601 dateTime, when the field work began + ActivityEnd string `json:"activityEnd"` // ISO 8601 dateTime, when the field work ended + Operations []OrderOperation `json:"operations,omitempty"` // unchanged; passed through to the nurse for awareness } // CompletionEvent is POSTed to the consumer's monitor endpoint when an order reaches TECO. diff --git a/sapper/thing.go b/sapper/thing.go index ee9451e..bd20425 100644 --- a/sapper/thing.go +++ b/sapper/thing.go @@ -144,9 +144,19 @@ const firefightingHTML = ` ` -// defaultEnrichmentJSON pre-fills the textarea with Alex's seals/gaskets -// example payload. Edit-in-place is the planner's main job. -const defaultEnrichmentJSON = `{ +// defaultEnrichmentTemplate is the editable JSON the planner sees in the +// textarea. The Sapper substitutes per-order context (currentPart IRI, date +// window) before rendering; the planner edits decision/newPart in place and +// submits. Decision "repair" leaves the same physical part fitted with +// service work done on it; "replace" swaps in newPart and marks the old part +// as no longer current. See Triona's WorkOrder_Repair_*.ttl and +// WorkOrder_Replace_*.ttl for the underlying graph mutations. +const defaultEnrichmentTemplate = `{ + "decision": "repair", + "currentPart": "%s", + "newPart": "%s", + "activityStart": "%s", + "activityEnd": "%s", "operations": [ { "operation": "0010", @@ -166,7 +176,7 @@ const defaultEnrichmentJSON = `{ }, { "operation": "0030", - "description": "Replace gaskets and stem", + "description": "Replace gaskets and stem (repair) or swap in newPart (replace)", "plannedWorkQuantity": "1.0", "workQuantityUnit": "H", "workCenter": "VALVE-WC01", @@ -179,14 +189,6 @@ const defaultEnrichmentJSON = `{ "unit": "EA", "plant": "1000", "storageLocation": "0001" - }, - { - "material": "VALVE-STEM-KIT", - "description": "Valve stem repair kit", - "requiredQuantity": "1.0", - "unit": "EA", - "plant": "1000", - "storageLocation": "0001" } ] }, @@ -196,22 +198,30 @@ const defaultEnrichmentJSON = `{ "plannedWorkQuantity": "1.5", "workQuantityUnit": "H", "workCenter": "VALVE-WC01", - "plant": "1000", - "components": [ - { - "material": "THREAD-SEALANT", - "description": "Thread sealant compound", - "requiredQuantity": "1.0", - "unit": "EA", - "plant": "1000", - "storageLocation": "0001" - } - ] + "plant": "1000" } ] } ` +// buildOrderEnrichmentTemplate fills the per-order placeholders in +// defaultEnrichmentTemplate. Called when an order is created so the +// firefighting UI shows context-appropriate JSON when the planner clicks the +// row. The newPart default is order-specific so repeated demo runs don't +// keep installing the same IRI at the same FL (which would make the +// currently-fitted-part lookup ambiguous on the next run). +func buildOrderEnrichmentTemplate(currentPartIRI, orderID string, when time.Time) string { + if currentPartIRI == "" { + // Sapper couldn't resolve the part; the planner has to fill it in. + currentPartIRI = "REPLACE-WITH-CURRENT-PART-IRI" + } + // Default activity window: a notional 4-hour shift today, in UTC. + start := when.UTC().Format("2006-01-02") + "T08:00:00Z" + end := when.UTC().Format("2006-01-02") + "T12:00:00Z" + newPart := "https://arrowheadweb.org/data/SK_NEW_" + orderID + return fmt.Sprintf(defaultEnrichmentTemplate, currentPartIRI, newPart, start, end) +} + var firefightingTemplate = template.Must(template.New("firefighting").Parse(firefightingHTML)) //-------------------------------------Define the unit asset @@ -358,6 +368,78 @@ func (t *Traits) primeCounterFromGraph() { } } +// lookupCurrentPart asks GraphDB for the IndividualPartView currently fitted +// at the given functional-location IRI. "Currently fitted" is defined the +// same way Triona's TECO queries define it: a BreakdownElementRealization +// whose breakdown item is the FL, whose RealizedAs IndividualPartView has an +// EffectivityAssignment / DatedEffectivity with a StartDefinition but no +// EndDefinition. Returns the IndividualPart IRI on success, or an error if +// the FL isn't modelled, has no currently-fitted part, or has more than one +// (ambiguous). Used to pre-fill the firefighting UI's enrichment template. +func (t *Traits) lookupCurrentPart(flIRI string) (string, error) { + if t.GraphDBURL == "" { + return "", fmt.Errorf("graphDbUrl not configured") + } + query := fmt.Sprintf(`PREFIX step: +PREFIX ber: +PREFIX eff: +PREFIX de: +SELECT DISTINCT ?part WHERE { + GRAPH { + ?br ber:RealizedAs ?part ; + ber:BreakdownItem <%s> . + } + GRAPH { + ?ea eff:AssignedTo ?br ; + eff:AssignedEffectivity ?de . + FILTER NOT EXISTS { ?de de:EndDefinition ?_ } + } +}`, flIRI) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.GraphDBURL, strings.NewReader(query)) + if err != nil { + return "", fmt.Errorf("build request: %w", err) + } + req.Header.Set("Content-Type", "application/sparql-query") + req.Header.Set("Accept", "application/sparql-results+json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("HTTP %s: %s", resp.Status, strings.TrimSpace(string(body))) + } + + var result struct { + Results struct { + Bindings []map[string]struct { + Value string `json:"value"` + } `json:"bindings"` + } `json:"results"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("decode response: %w", err) + } + bs := result.Results.Bindings + switch len(bs) { + case 0: + return "", fmt.Errorf("no currently-fitted part for FL <%s>", flIRI) + case 1: + iri := bs[0]["part"].Value + if iri == "" { + return "", fmt.Errorf("empty part binding") + } + return iri, nil + default: + return "", fmt.Errorf("ambiguous currently-fitted part for FL <%s> (%d matches)", flIRI, len(bs)) + } +} + // peekMaxOrderID queries GraphDB for the highest order number already // recorded as a step:WorkOrder under our IRI namespace. Returns (n, true) on // success, (0, true) when the graph is empty, (0, false) on any error. @@ -435,13 +517,25 @@ func (t *Traits) nextNotifID() string { // lifecycle countdown — the order waits in CRTD until a planner enriches and // releases it through the firefighting UI. func (t *Traits) createOrder(req OrderRequest) *Order { + now := time.Now() + currentPart := "" + if req.FunctionalLocationIRI != "" { + if p, err := t.lookupCurrentPart(req.FunctionalLocationIRI); err != nil { + log.Printf("sapper: cannot resolve current part for FL %s: %v", req.FunctionalLocationIRI, err) + } else { + currentPart = p + log.Printf("sapper: FL %s currently has part %s fitted", req.FunctionalLocationIRI, currentPart) + } + } + orderID := t.nextOrderID() o := &Order{ - ID: t.nextOrderID(), + ID: orderID, Notification: t.nextNotifID(), Status: "CRTD", - CreatedAt: time.Now(), + CreatedAt: now, Request: req, - SuggestedEnrichment: defaultEnrichmentJSON, + CurrentPartIRI: currentPart, + SuggestedEnrichment: buildOrderEnrichmentTemplate(currentPart, orderID, now), } t.mu.Lock() t.orders[o.ID] = o @@ -467,6 +561,15 @@ func (t *Traits) enrichAndRelease(orderID string, enrichment json.RawMessage) (* o.Status = "REL" o.ReleasedAt = time.Now() o.Enrichment = enrichment + // Parse the structured fields. A parse failure isn't fatal — the TECO + // path falls back to the simpler status-only update when ParsedEnrichment + // is nil or doesn't carry the fields a Repair/Replace template needs. + var parsed Enrichment + if err := json.Unmarshal(enrichment, &parsed); err == nil { + o.ParsedEnrichment = &parsed + } else { + log.Printf("enrichAndRelease: order %s enrichment is not structured (%v); TECO will use fallback template", o.ID, err) + } t.mu.Unlock() log.Printf("order %s → REL (planner authorised)\n", o.ID) @@ -557,13 +660,23 @@ func (t *Traits) notifyConsumer(o *Order) { //-------------------------------------GraphDB SPARQL insert // buildSPARQL constructs the SPARQL UPDATE statement for a newly created order. -// The WorkRequestAssignment block — which links the order to a functional -// location in the STEP graph — is only emitted when the consumer supplied an -// IRI. Orders raised by consumers that don't resolve an FL IRI still get a -// WorkOrder and WorkRequest, just no graph-side asset linkage. +// URI shapes follow Triona's convention `/_` so tools that +// expect `WorkRequestAssignment_`, `ID_`, etc. find our +// data through the same patterns. The WorkRequestAssignment block — which +// links the order to a functional location in the STEP graph — is only +// emitted when the consumer supplied an IRI; orders raised by consumers that +// don't resolve an FL IRI still get a WorkOrder and WorkRequest, just no +// graph-side asset linkage. func (t *Traits) buildSPARQL(o *Order) string { - orderURI := "https://sinetiq.se/sap/MaintenanceOrder/" + o.ID - notifURI := "https://sinetiq.se/sap/MaintenanceNotification/" + o.Notification + const woBase = "https://sinetiq.se/sap/MaintenanceOrder/" + const wrBase = "https://sinetiq.se/sap/MaintenanceNotification/" + orderURI := woBase + o.ID + orderIDURI := woBase + "ID_" + o.ID + orderDescURI := woBase + "Description_" + o.ID + notifURI := wrBase + o.Notification + notifIDURI := wrBase + "ID_" + o.Notification + notifDescURI := wrBase + "Description_" + o.Notification + wraURI := wrBase + "WorkRequestAssignment_" + o.Notification createdAt := o.CreatedAt.UTC().Format(time.RFC3339Nano) desc := strings.ReplaceAll(o.Request.Description, `"`, `\"`) // escape any quotes @@ -571,10 +684,10 @@ func (t *Traits) buildSPARQL(o *Order) string { if o.Request.FunctionalLocationIRI != "" { wraBlock = fmt.Sprintf(` # step:WorkRequestAssignment - <%s-WRA> a step:WorkRequestAssignment ; + <%s> a step:WorkRequestAssignment ; workrequestassignment:AssignedTo <%s> ; workrequestassignment:AssignedWorkRequest <%s> .`, - notifURI, o.Request.FunctionalLocationIRI, notifURI) + wraURI, o.Request.FunctionalLocationIRI, notifURI) } return fmt.Sprintf(`PREFIX rdfs: @@ -593,28 +706,28 @@ INSERT { # step:WorkOrder <%s> a step:WorkOrder ; - workorder:Id <%s-id> ; - workorder:Description <%s-description> ; + workorder:Id <%s> ; + workorder:Description <%s> ; ex:status "%s" ; dcterms:created "%s"^^xsd:dateTime ; workorder:InResponseTo <%s> . - <%s-id> a step:Identifier ; + <%s> a step:Identifier ; identifier:Id "%s" . - <%s-description> a step:LocalizedString ; + <%s> a step:LocalizedString ; rdfs:label "Maintenance order created successfully"@en . # step:WorkRequest <%s> a step:WorkRequest ; - workrequest:Id <%s-id> ; - workrequest:Description <%s-description> ; + workrequest:Id <%s> ; + workrequest:Description <%s> ; dcterms:isPartOf <%s> . - <%s-id> a step:Identifier ; + <%s> a step:Identifier ; identifier:Id "%s" . - <%s-description> a step:LocalizedString ; + <%s> a step:LocalizedString ; rdfs:label "%s"@en .%s } } @@ -622,18 +735,18 @@ where { }`, // WorkOrder - orderURI, orderURI, orderURI, + orderURI, orderIDURI, orderDescURI, o.Status, createdAt, notifURI, - // WorkOrder-id - orderURI, o.ID, - // WorkOrder-description - orderURI, + // WorkOrder Identifier + orderIDURI, o.ID, + // WorkOrder Description + orderDescURI, // WorkRequest - notifURI, notifURI, notifURI, orderURI, - // WorkRequest-id - notifURI, o.Notification, - // WorkRequest-description - notifURI, desc, + notifURI, notifIDURI, notifDescURI, orderURI, + // WorkRequest Identifier + notifIDURI, o.Notification, + // WorkRequest Description + notifDescURI, desc, // Optional WorkRequestAssignment wraBlock, ) @@ -717,24 +830,28 @@ func (t *Traits) notifyEnrichment(o *Order) { log.Printf("← enrichment %s body=%s\n", resp.Status, string(msg)) } -// buildTECOInsertSPARQL records an order's transition to TECO. As with REL, -// the status triple replaces the previous one (REL or CRTD, whichever the -// order was sitting on) so the UI sees the current state. The completion -// timestamp, modified timestamp, and actual work hours are additive — they -// don't conflict with any prior triples on the order. -func (t *Traits) buildTECOInsertSPARQL(o *Order) string { +// buildTECOFallbackSPARQL writes the simplest possible TECO update: status +// flip + completion timestamp + modified. Used when the planner's enrichment +// didn't include the parts/dates needed to construct a Repair or Replace +// update, or when the order's FunctionalLocationIRI couldn't be resolved. +// Keeps the demo's loop intact even without Triona's part graphs being +// populated for the FL we targeted. +func (t *Traits) buildTECOFallbackSPARQL(o *Order) string { orderURI := "https://sinetiq.se/sap/MaintenanceOrder/" + o.ID + descURI := "https://sinetiq.se/sap/MaintenanceOrder/Description_" + o.ID completedAt := time.Now().UTC().Format(time.RFC3339Nano) - actualHours := float64(t.CompletionDelay) / 3600.0 // CompletionDelay is in seconds - + actualHours := float64(t.CompletionDelay) / 3600.0 return fmt.Sprintf(`PREFIX ex: PREFIX xsd: PREFIX dcterms: +PREFIX rdfs: DELETE { GRAPH { - <%s> ex:status ?old . + <%s> ex:status ?oldStatus . + <%s> rdfs:label ?oldLabel . }} WHERE { GRAPH { - <%s> ex:status ?old . + OPTIONAL { <%s> ex:status ?oldStatus } + OPTIONAL { <%s> rdfs:label ?oldLabel } }}; INSERT DATA { GRAPH { @@ -742,7 +859,410 @@ INSERT DATA { GRAPH { ex:completedAt "%s"^^xsd:dateTime ; dcterms:modified "%s"^^xsd:dateTime ; ex:actualWorkHours "%g"^^xsd:decimal . -}}`, orderURI, orderURI, orderURI, completedAt, completedAt, actualHours) + <%s> rdfs:label "Maintenance order completed"@en . +}}`, + orderURI, descURI, + orderURI, descURI, + orderURI, completedAt, completedAt, actualHours, descURI) +} + +// buildTECORepairSPARQL produces the Repair-variant TECO update — mirror of +// Triona's WorkOrder_Repair_*.ttl. Same physical part stays fitted; the +// graph gains an ActualActivity recording when the service work happened, +// linked through DirectedPlannedActivity to the work order, plus a +// repair-activity DatedEffectivity in the effectivity graph. The currently +// fitted part's own effectivity is NOT modified. +// +// Inputs come from the planner's enrichment JSON: currentPart IRI plus +// activity start/end timestamps. The WHERE clause validates that this part +// is in fact currently fitted at the same breakdown item the work request +// points at — a graph-side integrity check that the planner picked a +// sensible part. +func (t *Traits) buildTECORepairSPARQL(o *Order, e *Enrichment) string { + orderURI := "https://sinetiq.se/sap/MaintenanceOrder/" + o.ID + woAssignURI := "https://sinetiq.se/sap/MaintenanceOrder/WorkOrderAssignment_" + o.ID + completedAt := time.Now().UTC().Format(time.RFC3339Nano) + + return fmt.Sprintf(`PREFIX : +PREFIX ex: +PREFIX dcterms: +PREFIX rdfs: +PREFIX step: +PREFIX workorder: +PREFIX wra: +PREFIX dpa: +PREFIX ahr: +PREFIX aa: +PREFIX actualactivity: +PREFIX eff: +PREFIX de: +PREFIX ber: +PREFIX pva: +PREFIX woa: +PREFIX xsd: + +DELETE { + GRAPH { + ?workOrder ex:status ?oldStatus . + ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate . + ?workOrder dcterms:modified ?oldModified . + ?workOrderDescription rdfs:label ?oldDescriptionLabel . + } +} +INSERT { + GRAPH { + ?workOrder + ex:status ?newStatus ; + ex:technicalCompletionDate ?technicalCompletionDateTime ; + dcterms:modified ?modifiedDateTime . + + ?workOrderDescription rdfs:label "Maintenance order completed (repair)"@en . + + ?directedPlannedActivity + a step:DirectedPlannedActivity ; + dpa:Directive ?workOrder . + + ?actualActivity + a step:ActualActivity ; + actualactivity:ActualStartDate [ + a step:DateTimeString, step:String ; + :value ?activityStartDateTime + ] ; + actualactivity:ActualEndDate [ + a step:DateTimeString, step:String ; + :value ?activityEndDateTime + ] . + + ?activityHappeningRelationship + a step:ActivityHappeningRelationship ; + ahr:Relating ?directedPlannedActivity ; + ahr:Related ?actualActivity . + + ?activityAssignmentRepairedPart + a step:ActivityAssignment ; + aa:AssignedTo ?individualPart ; + aa:AssignedActivity ?actualActivity . + + ?activityAssignmentRepairActivityEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?repairActivityEffectivity ; + aa:AssignedActivity ?actualActivity . + + ?workOrderAssignment + a step:WorkOrderAssignment ; + woa:AssignedTo ?breakdownItem ; + woa:AssignedWorkOrder ?workOrder . + } + + GRAPH { + ?repairActivityEffectivity + a step:EffectivityAssignment ; + eff:AssignedTo ?actualActivity ; + eff:AssignedEffectivity ?repairActivityDatedEffectivity ; + eff:EffectivityIndication step:True . + + ?repairActivityDatedEffectivity + a step:DatedEffectivity ; + de:StartDefinition ?repairActivityStartDateTimeString ; + de:EndDefinition ?repairActivityEndDateTimeString . + + ?repairActivityStartDateTimeString + a step:DateTimeString, step:String ; + :value ?activityStartDateTime . + + ?repairActivityEndDateTimeString + a step:DateTimeString, step:String ; + :value ?activityEndDateTime . + } +} +WHERE { + BIND(IRI("%s") AS ?workOrder) + BIND(IRI("%s") AS ?workOrderAssignment) + BIND(IRI("%s") AS ?individualPart) + + BIND("TECO" AS ?newStatus) + BIND("%s"^^xsd:dateTime AS ?technicalCompletionDateTime) + BIND("%s"^^xsd:dateTime AS ?modifiedDateTime) + BIND("%s"^^xsd:dateTime AS ?activityStartDateTime) + BIND("%s"^^xsd:dateTime AS ?activityEndDateTime) + + BIND(REPLACE(STR(?workOrder), "^.*/", "") AS ?workOrderId) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/Description_", ?workOrderId)) AS ?workOrderDescription) + + OPTIONAL { GRAPH { ?workOrder ex:status ?oldStatus } } + OPTIONAL { GRAPH { ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate } } + OPTIONAL { GRAPH { ?workOrder dcterms:modified ?oldModified } } + OPTIONAL { GRAPH { ?workOrderDescription rdfs:label ?oldDescriptionLabel } } + + GRAPH { + ?individualPart a step:IndividualPartView . + } + + GRAPH { + ?partRealization + pva:AssociatedIndividualPart ?individualPart ; + pva:AssociatedPart ?part . + ?breakdownRealization + ber:RealizedAs ?individualPart ; + ber:BreakdownItem ?breakdownItem . + } + + GRAPH { + ?workOrder a step:WorkOrder ; + workorder:InResponseTo ?workRequest . + ?workRequestAssignment + a step:WorkRequestAssignment ; + wra:AssignedWorkRequest ?workRequest ; + wra:AssignedTo ?breakdownItem . + } + + GRAPH { + ?effCurrent + eff:AssignedTo ?breakdownRealization ; + eff:AssignedEffectivity ?currentDatedEffectivity . + FILTER NOT EXISTS { ?currentDatedEffectivity de:EndDefinition ?_existingEndDefinition } + } + + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DirectedPlannedActivity_", ?workOrderId)) AS ?directedPlannedActivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActivityHappeningRelationship_", ?workOrderId)) AS ?activityHappeningRelationship) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActualActivity_", ?workOrderId)) AS ?actualActivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", ?workOrderId, "_RepairedPart")) AS ?activityAssignmentRepairedPart) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", ?workOrderId, "_RepairActivityEffectivity")) AS ?activityAssignmentRepairActivityEffectivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/EffectivityAssignment_", ?workOrderId, "_RepairActivity")) AS ?repairActivityEffectivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DatedEffectivity_", ?workOrderId, "_RepairActivity")) AS ?repairActivityDatedEffectivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_Start_", ?workOrderId, "_RepairActivity")) AS ?repairActivityStartDateTimeString) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_End_", ?workOrderId, "_RepairActivity")) AS ?repairActivityEndDateTimeString) +}`, + orderURI, woAssignURI, e.CurrentPart, + completedAt, completedAt, + e.ActivityStart, e.ActivityEnd) +} + +// buildTECOReplaceSPARQL produces the Replace-variant TECO update — mirror +// of Triona's WorkOrder_Replace_*.ttl. The old part's currently-active +// effectivity gets an EndDefinition (no longer fitted); a new +// IndividualPartView is created with start-only effectivity (now currently +// fitted); ido-ext:implements alignment triples link the new part to its +// breakdown item and PartView. Significant graph mutation; depends on +// Triona's part/association/effectivity graphs being correctly populated for +// the FL in question. +func (t *Traits) buildTECOReplaceSPARQL(o *Order, e *Enrichment) string { + orderURI := "https://sinetiq.se/sap/MaintenanceOrder/" + o.ID + woAssignURI := "https://sinetiq.se/sap/MaintenanceOrder/WorkOrderAssignment_" + o.ID + completedAt := time.Now().UTC().Format(time.RFC3339Nano) + + return fmt.Sprintf(`PREFIX : +PREFIX ex: +PREFIX dcterms: +PREFIX rdfs: +PREFIX ido-ext: +PREFIX step: +PREFIX workorder: +PREFIX wra: +PREFIX dpa: +PREFIX ahr: +PREFIX aa: +PREFIX actualactivity: +PREFIX eff: +PREFIX de: +PREFIX ber: +PREFIX pva: +PREFIX woa: +PREFIX xsd: + +DELETE { + GRAPH { + ?workOrder ex:status ?oldStatus . + ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate . + ?workOrder dcterms:modified ?oldModified . + ?workOrderDescription rdfs:label ?oldDescriptionLabel . + } +} +INSERT { + GRAPH { + ?workOrder + ex:status ?newStatus ; + ex:technicalCompletionDate ?technicalCompletionDateTime ; + dcterms:modified ?modifiedDateTime . + + ?workOrderDescription rdfs:label "Maintenance order completed (replacement)"@en . + + ?directedPlannedActivity + a step:DirectedPlannedActivity ; + dpa:Directive ?workOrder . + + ?actualActivity + a step:ActualActivity ; + actualactivity:ActualStartDate [ + a step:DateTimeString, step:String ; + :value ?activityStartDateTime + ] ; + actualactivity:ActualEndDate [ + a step:DateTimeString, step:String ; + :value ?activityEndDateTime + ] . + + ?activityHappeningRelationship + a step:ActivityHappeningRelationship ; + ahr:Relating ?directedPlannedActivity ; + ahr:Related ?actualActivity . + + ?activityAssignmentOutgoingEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?effOld ; + aa:AssignedActivity ?actualActivity . + + ?activityAssignmentIncomingEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?effNew ; + aa:AssignedActivity ?actualActivity . + + ?activityAssignmentReplacementActivityEffectivity + a step:ActivityAssignment ; + aa:AssignedTo ?replacementActivityEffectivity ; + aa:AssignedActivity ?actualActivity . + + ?workOrderAssignment + a step:WorkOrderAssignment ; + woa:AssignedTo ?breakdownItem ; + woa:AssignedWorkOrder ?workOrder . + } + + GRAPH { + ?newIndividualPart a step:IndividualPartView, ido-ext:IndividualPart . + } + + GRAPH { + ?newRealization + a :AssociationObject, step:BreakdownElementRealization ; + ber:RealizedAs ?newIndividualPart ; + ber:BreakdownItem ?breakdownItem . + ?newPvIpvAssociation + a :AssociationObject, step:PartViewToIndividualPartViewAssociation ; + pva:AssociatedIndividualPart ?newIndividualPart ; + pva:AssociatedPart ?part . + } + + GRAPH { + ?effOldEffectivity de:EndDefinition ?oldEndDateTimeString . + ?oldEndDateTimeString + a step:DateTimeString, step:String ; + :value ?activityEndDateTime . + + ?effNew + a step:EffectivityAssignment ; + eff:AssignedTo ?newRealization ; + eff:AssignedEffectivity ?newDatedEffectivity ; + eff:EffectivityIndication step:True . + + ?newDatedEffectivity + a step:DatedEffectivity ; + de:StartDefinition ?newStartDateTimeString . + + ?newStartDateTimeString + a step:DateTimeString, step:String ; + :value ?activityEndDateTime . + + ?replacementActivityEffectivity + a step:EffectivityAssignment ; + eff:AssignedTo ?actualActivity ; + eff:AssignedEffectivity ?replacementActivityDatedEffectivity ; + eff:EffectivityIndication step:True . + + ?replacementActivityDatedEffectivity + a step:DatedEffectivity ; + de:StartDefinition ?replacementActivityStartDateTimeString ; + de:EndDefinition ?replacementActivityEndDateTimeString . + + ?replacementActivityStartDateTimeString + a step:DateTimeString, step:String ; + :value ?activityStartDateTime . + + ?replacementActivityEndDateTimeString + a step:DateTimeString, step:String ; + :value ?activityEndDateTime . + } + + GRAPH { + ?newIndividualPart ido-ext:implements ?breakdownItem . + ?newIndividualPart ido-ext:implements ?part . + } +} +WHERE { + BIND(IRI("%s") AS ?workOrder) + BIND(IRI("%s") AS ?workOrderAssignment) + BIND(IRI("%s") AS ?individualPart) + BIND(IRI("%s") AS ?newIndividualPart) + + BIND("TECO" AS ?newStatus) + BIND("%s"^^xsd:dateTime AS ?technicalCompletionDateTime) + BIND("%s"^^xsd:dateTime AS ?modifiedDateTime) + BIND("%s"^^xsd:dateTime AS ?activityStartDateTime) + BIND("%s"^^xsd:dateTime AS ?activityEndDateTime) + + BIND(REPLACE(STR(?workOrder), "^.*/", "") AS ?workOrderId) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/Description_", ?workOrderId)) AS ?workOrderDescription) + + OPTIONAL { GRAPH { ?workOrder ex:status ?oldStatus } } + OPTIONAL { GRAPH { ?workOrder ex:technicalCompletionDate ?oldTechnicalCompletionDate } } + OPTIONAL { GRAPH { ?workOrder dcterms:modified ?oldModified } } + OPTIONAL { GRAPH { ?workOrderDescription rdfs:label ?oldDescriptionLabel } } + + GRAPH { + ?individualPart a step:IndividualPartView . + } + + GRAPH { + ?partRealization + pva:AssociatedIndividualPart ?individualPart ; + pva:AssociatedPart ?part . + ?breakdownRealization + ber:RealizedAs ?individualPart ; + ber:BreakdownItem ?breakdownItem . + } + + GRAPH { + ?workOrder a step:WorkOrder ; + workorder:InResponseTo ?workRequest . + ?workRequestAssignment + a step:WorkRequestAssignment ; + wra:AssignedWorkRequest ?workRequest ; + wra:AssignedTo ?breakdownItem . + } + + GRAPH { + ?effOld + eff:AssignedTo ?breakdownRealization ; + eff:AssignedEffectivity ?effOldEffectivity . + FILTER NOT EXISTS { ?effOldEffectivity de:EndDefinition ?_existingEndDefinition } + } + + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DirectedPlannedActivity_", ?workOrderId)) AS ?directedPlannedActivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActivityHappeningRelationship_", ?workOrderId)) AS ?activityHappeningRelationship) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActualActivity_", ?workOrderId)) AS ?actualActivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", ?workOrderId, "_OutgoingEffectivity")) AS ?activityAssignmentOutgoingEffectivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", ?workOrderId, "_IncomingEffectivity")) AS ?activityAssignmentIncomingEffectivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/ActivityAssignment_", ?workOrderId, "_ReplacementActivityEffectivity")) AS ?activityAssignmentReplacementActivityEffectivity) + + BIND(IRI(CONCAT(STR(?breakdownRealization), "-replacement")) AS ?newRealization) + BIND(IRI(CONCAT(STR(?partRealization), "-replacement")) AS ?newPvIpvAssociation) + + BIND(REPLACE(STR(?breakdownRealization), "^.*[/#]", "") AS ?oldRealizationId) + BIND(REPLACE(STR(?newRealization), "^.*[/#]", "") AS ?newRealizationId) + + BIND(IRI(CONCAT("https://arrowheadweb.org/data/DateTimeString_End_", ENCODE_FOR_URI(?oldRealizationId))) AS ?oldEndDateTimeString) + BIND(IRI(CONCAT("https://arrowheadweb.org/data/EffectivityAssignment_", ENCODE_FOR_URI(?newRealizationId))) AS ?effNew) + BIND(IRI(CONCAT("https://arrowheadweb.org/data/DatedEffectivity_", ENCODE_FOR_URI(?newRealizationId))) AS ?newDatedEffectivity) + BIND(IRI(CONCAT("https://arrowheadweb.org/data/DateTimeString_", ENCODE_FOR_URI(?newRealizationId))) AS ?newStartDateTimeString) + + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/EffectivityAssignment_", ?workOrderId, "_ReplacementActivity")) AS ?replacementActivityEffectivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DatedEffectivity_", ?workOrderId, "_ReplacementActivity")) AS ?replacementActivityDatedEffectivity) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_Start_", ?workOrderId, "_ReplacementActivity")) AS ?replacementActivityStartDateTimeString) + BIND(IRI(CONCAT("https://sinetiq.se/sap/MaintenanceOrder/DateTimeString_End_", ?workOrderId, "_ReplacementActivity")) AS ?replacementActivityEndDateTimeString) +}`, + orderURI, woAssignURI, e.CurrentPart, e.NewPart, + completedAt, completedAt, + e.ActivityStart, e.ActivityEnd) } // updateEndpoint returns the SPARQL update URL by appending /statements to the @@ -761,9 +1281,45 @@ func (t *Traits) insertToGraphDB(o *Order) { } // insertCompletionToGraphDB records the order's TECO transition in the graph. -// Called from runLifecycle once the in-memory status flips to TECO. +// Called from runTECOCountdown once the in-memory status flips to TECO. The +// SPARQL template chosen depends on what the planner specified in the +// enrichment JSON: +// +// - decision="replace" with both currentPart and newPart populated → Replace template +// (closes old part's effectivity, creates new IndividualPartView, etc.) +// - decision="repair" with currentPart populated → Repair template +// (records ActualActivity against the same part; effectivity unchanged) +// - anything else (no parsed enrichment, missing parts, missing dates) → fallback +// template (status flip + completion timestamp only) +// +// Falling back is intentional: it keeps the demo loop alive even when +// Triona-side data dependencies (parts / associations / effectivity graphs) +// aren't yet in place for the target FL. func (t *Traits) insertCompletionToGraphDB(o *Order) { - t.postSPARQL("TECO", o.ID, t.buildTECOInsertSPARQL(o)) + stage, sparql := t.chooseTECOTemplate(o) + t.postSPARQL(stage, o.ID, sparql) +} + +// chooseTECOTemplate picks the most specific TECO SPARQL template the +// available data supports, and returns it along with a tag for the log line. +func (t *Traits) chooseTECOTemplate(o *Order) (stage, sparql string) { + e := o.ParsedEnrichment + if e == nil || e.CurrentPart == "" || e.ActivityStart == "" || e.ActivityEnd == "" { + return "TECO", t.buildTECOFallbackSPARQL(o) + } + switch strings.ToLower(e.Decision) { + case "replace": + if e.NewPart == "" { + log.Printf("sapper: order %s decision=replace but newPart is empty; using fallback TECO", o.ID) + return "TECO", t.buildTECOFallbackSPARQL(o) + } + return "TECO-replace", t.buildTECOReplaceSPARQL(o, e) + case "repair", "": + return "TECO-repair", t.buildTECORepairSPARQL(o, e) + default: + log.Printf("sapper: order %s unknown decision %q; using fallback TECO", o.ID, e.Decision) + return "TECO", t.buildTECOFallbackSPARQL(o) + } } // postSPARQL is the common path for both lifecycle pushes. Graph publication is diff --git a/telegrapher/go.mod b/telegrapher/go.mod index 1cd306b..38f42b7 100644 --- a/telegrapher/go.mod +++ b/telegrapher/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/telegrapher -go 1.26.3 +go 1.26.4 require ( github.com/eclipse/paho.mqtt.golang v1.5.1 diff --git a/telegrapher/mqttGen/go.mod b/telegrapher/mqttGen/go.mod index a72ab90..071f1b9 100644 --- a/telegrapher/mqttGen/go.mod +++ b/telegrapher/mqttGen/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/telegrapher/mqttGen -go 1.22.6 +go 1.26.4 require github.com/eclipse/paho.mqtt.golang v1.5.0 diff --git a/thermostat/go.mod b/thermostat/go.mod index 53edc79..8dcd530 100644 --- a/thermostat/go.mod +++ b/thermostat/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/thermostat -go 1.26.3 +go 1.26.4 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/tracker/go.mod b/tracker/go.mod index 015cb8f..f1fd965 100644 --- a/tracker/go.mod +++ b/tracker/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/tracker -go 1.26.3 +go 1.26.4 require ( github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/uaclient/go.mod b/uaclient/go.mod index bf28c80..aff6c86 100644 --- a/uaclient/go.mod +++ b/uaclient/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/uaclient -go 1.26.3 +go 1.26.4 require ( github.com/gopcua/opcua v0.6.5 diff --git a/weatherman/go.mod b/weatherman/go.mod index 5f9759b..67ecf46 100644 --- a/weatherman/go.mod +++ b/weatherman/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/weatherman -go 1.26.3 +go 1.26.4 require ( github.com/sdoque/mbaigo v0.1.0-alpha.7