-
Notifications
You must be signed in to change notification settings - Fork 74
Feat/diff #521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Feat/diff #521
Changes from all commits
32b3b6e
b4f1df3
0f8565e
cd6b2a1
3b4b53b
50cc075
1dedb88
7b94ff3
08cd616
2099b8a
b77d9fe
4423895
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # compose | ||
|
|
||
| ## What it does | ||
|
|
||
| A vspec model is typically spread across many files: a main `.vspec`, optional `#include` files, overlays, a units file, and a quantities file. `vspec compose` collapses all of that into a single **self-contained folder** (a _snapshot_) that any downstream `vspec export` command can consume without needing the original source tree. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth stating the differences compared to the "vspec export yaml" command, because they have some similarities, right? Like long term, do we need both? |
||
|
|
||
| ``` | ||
| vspec compose \ | ||
| -u units.yaml \ | ||
| -q quantities.yaml \ | ||
| -s spec/Vehicle.vspec \ | ||
| -l my_overlay.vspec \ | ||
| --types types.vspec \ | ||
| -o snapshot/ | ||
| ``` | ||
|
|
||
| The output folder contains up to four files: | ||
|
|
||
| | File | Contains | Always written? | | ||
| |---|---|---| | ||
| | `model_snapshot.vspec` | The full model tree | Yes | | ||
| | `structs_snapshot.vspec` | Custom struct type definitions | Only when `--types` is given | | ||
| | `units_snapshot.yaml` | Resolved units | When `-u` is given or `units.yaml` exists next to the vspec | | ||
| | `quantities_snapshot.yaml` | Resolved quantities | When `-q` is given or `quantities.yaml` exists next to the vspec | | ||
|
|
||
| > Note: `vss-tools` automatically looks for `units.yaml` and `quantities.yaml` next to the vspec file when no `-u`/`-q` flags are given. If neither explicit paths nor default files can be found, the corresponding snapshot files are simply not written. | ||
|
|
||
|
|
||
| ## How it works | ||
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| A[Source .vspec + overlays + units/quantities] --> B[Phase 1: validate\nget_trees expand=True] | ||
| B -->|invalid model| C[Abort with error] | ||
| B -->|valid| D[Phase 2: raw serialization\nload_vspec direct] | ||
| D --> E[snapshot/model_snapshot.vspec] | ||
| D --> F[snapshot/structs_snapshot.vspec] | ||
| D --> G[snapshot/units_snapshot.yaml] | ||
| D --> H[snapshot/quantities_snapshot.yaml] | ||
| ``` | ||
|
|
||
| **Phase 1 — Validation.** The full model is loaded with expansion enabled. All Pydantic type checks, unit validation, naming conventions, and structural rules run normally. If anything is wrong the command aborts with a clear error — the snapshot is never written. | ||
|
|
||
| **Phase 2 — Faithful serialization.** The raw authored YAML is read directly (without tree-building) and written as-is. This means: | ||
|
|
||
| - `instances` fields are preserved — the snapshot is not pre-expanded. | ||
| - Instance-level overrides (e.g. `Door.Row1.Left.IsOpen`) written in the source are kept exactly as authored. | ||
| - Only internal runtime fields (`delete`, `fqn`, `is_instance`) are stripped. | ||
| - No default values are injected — what you wrote is what you get. | ||
|
|
||
| ## Using the snapshot | ||
|
|
||
| The primary use of such a snapshot could be a released artifact to complement the versioning and evolution of a domain data model. | ||
| Once the snapshot is taken, it becomes an inmutable reference. | ||
| Then, it becomes easier to report differences between two snapshopts. | ||
| See [diff command](./diff.md). | ||
|
|
||
| Because the snapshot is valid vspec, it feeds directly into any exporter: | ||
|
|
||
| ```bash | ||
| vspec export json \ | ||
| -u snapshot/units_snapshot.yaml \ | ||
| -q snapshot/quantities_snapshot.yaml \ | ||
| -s snapshot/model_snapshot.vspec \ | ||
| --types snapshot/structs_snapshot.vspec \ | ||
| -o output.json | ||
| ``` | ||
|
|
||
| This also means the snapshot round-trips cleanly: composing a snapshot of a snapshot produces identical output. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| # diff | ||
|
|
||
| ## What it does | ||
|
|
||
| `vspec diff` compares two snapshots produced by `vspec compose` and reports every change as a structured JSON document. It is the foundation for tracking model evolution, generating changelogs, and feeding a spec registry. | ||
|
|
||
| ```bash | ||
| vspec diff -p snapshot_v1/ -c snapshot_v2/ | ||
| # or write to file: | ||
| vspec diff -p snapshot_v1/ -c snapshot_v2/ -o changes.json | ||
| ``` | ||
|
|
||
| ## Output structure | ||
|
|
||
| ```json | ||
| { | ||
| "previous": "snapshot_v1/", | ||
| "current": "snapshot_v2/", | ||
| "summary": { "ADDED": 2, "REMOVED": 1, "MODIFIED": 3 }, | ||
| "changes": [ ... ] | ||
| } | ||
| ``` | ||
|
|
||
| Each entry in `changes` has: | ||
|
|
||
| | Field | Always present | Description | | ||
| |---|---|---| | ||
| | `type` | Yes | `ADDED`, `REMOVED`, or `MODIFIED` | | ||
| | `source` | Yes | `model`, `structs`, `units`, or `quantities` | | ||
| | `path` | Yes | FQN of the node in the **current** snapshot | | ||
| | `node_type` | Yes | e.g. `branch`, `sensor`, `actuator` | | ||
| | `message` | Yes | Human-readable description of the change | | ||
| | `previous_path` | MODIFIED renames only | FQN in the previous snapshot | | ||
| | `cascade` | MODIFIED renames only | `true` if this is a child of a renamed branch | | ||
| | `attribute_changes` | MODIFIED only | List of `{attribute, previous, current}` dicts | | ||
| | `attributes` | ADDED only | Full attribute dict of the new node | | ||
|
|
||
| ## Change types | ||
|
|
||
| **ADDED** — a node exists in current but not in previous. | ||
|
|
||
| **REMOVED** — a node exists in previous but not in current. | ||
|
|
||
| **MODIFIED** — a node exists in both, but something changed. This covers: | ||
| - attribute value changes (datatype, unit, description, etc.) | ||
| - renames detected via the `fka` ("formerly known as") field | ||
|
|
||
| ## Rename detection | ||
|
|
||
| Renames are detected automatically without requiring any extra flags. | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| subgraph previous | ||
| A[A.Door\ntype: branch] | ||
| B[A.Door.IsOpen\ntype: actuator] | ||
| end | ||
| subgraph current | ||
| C["A.Portal\ntype: branch\nfka: A.Door"] | ||
| D[A.Portal.IsOpen\ntype: actuator] | ||
| end | ||
| A -- "fka match + same node_type" --> C | ||
| B -- "prefix substitution\ncascade: true" --> D | ||
| ``` | ||
|
|
||
| 1. **Explicit rename** — if an added node has `fka: [old.path]` and the same `type` as the removed node, it is reported as `MODIFIED` with `previous_path`. | ||
| 2. **Cascade** — children of a renamed branch are matched by FQN prefix substitution. Each cascaded child is independently checked for attribute changes too. | ||
|
|
||
| If `fka` is missing, or the node type doesn't match, the pair is reported as independent `REMOVED` + `ADDED`. | ||
|
|
||
| ## Example output | ||
|
|
||
| ```json | ||
| { | ||
| "type": "MODIFIED", | ||
| "source": "model", | ||
| "path": "A.Portal", | ||
| "previous_path": "A.Door", | ||
| "node_type": "branch", | ||
| "cascade": false, | ||
| "attribute_changes": [], | ||
| "message": "Branch 'A.Door' was renamed to 'A.Portal'." | ||
| }, | ||
| { | ||
| "type": "MODIFIED", | ||
| "source": "model", | ||
| "path": "A.Portal.IsOpen", | ||
| "previous_path": "A.Door.IsOpen", | ||
| "node_type": "actuator", | ||
| "cascade": true, | ||
| "attribute_changes": [ | ||
| { "attribute": "datatype", "previous": "boolean", "current": "string" } | ||
| ], | ||
| "message": "Actuator 'A.Door.IsOpen' was renamed to 'A.Portal.IsOpen' (cascaded from parent rename). Attribute 'datatype' changed from 'boolean' to 'string'." | ||
| } | ||
| ``` | ||
|
|
||
| ## Typical workflow | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| S[Source files\nvspec + overlays] --> C1[vspec compose\nv1.0] | ||
| S2[Updated source files] --> C2[vspec compose\nv2.0] | ||
| C1 --> SN1[snapshot_v1/] | ||
| C2 --> SN2[snapshot_v2/] | ||
| SN1 --> D[vspec diff] | ||
| SN2 --> D | ||
| D --> J[changes.json\nfed to registry / changelog] | ||
| ``` | ||
|
|
||
| ## About braking changes | ||
| The `diff` command is intentionally reporting ANY change without dictating what constitudes a braking chage. | ||
| That distinction is to be handle elsewhere based on the diff report. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,15 @@ | |
| from vss_tools.lazy_group import LazyGroup | ||
|
|
||
|
|
||
| @click.group(context_settings={"auto_envvar_prefix": "vss_tools"}, invoke_without_command=True) | ||
| @click.group( | ||
| cls=LazyGroup, | ||
| lazy_subcommands={ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need the lazy annotations here. Just make the commands "normal" ones by using |
||
| "compose": "vss_tools.compose:cli", | ||
| "diff": "vss_tools.diff_cmd:cli", | ||
| }, | ||
| context_settings={"auto_envvar_prefix": "vss_tools"}, | ||
| invoke_without_command=True, | ||
| ) | ||
| @clo.log_level_opt | ||
| @clo.log_file_opt | ||
| @click.version_option() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.