From 5be5de26b91c5db34cd74b977374a88aee90201e Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Wed, 22 Jul 2026 12:07:59 +1200 Subject: [PATCH 1/7] Add design: close three validator gaps (savedqueries, AppModuleSiteMap, nested flow actions) --- .../specs/2026-07-22-validator-gaps-design.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-22-validator-gaps-design.md diff --git a/docs/superpowers/specs/2026-07-22-validator-gaps-design.md b/docs/superpowers/specs/2026-07-22-validator-gaps-design.md new file mode 100644 index 0000000..16f8aa8 --- /dev/null +++ b/docs/superpowers/specs/2026-07-22-validator-gaps-design.md @@ -0,0 +1,148 @@ +# Close three validator gaps (dataverse-xml-lsp + cloud-flow-json-lsp) + +Date: 2026-07-22 +Status: Approved for planning + +## Background + +Testing both plugins against a real client solution export (712 customization XML files +and 12 unpacked cloud flows) surfaced three gaps where a validator silently fails to +cover the most common real-export shapes. Each finding below was reproduced against the +installed plugins (cache paths, freshly downloaded schemas, LSP servers reloaded), so the +gaps are in the mapping/schema logic, not in setup. + +| Finding | Plugin | Real-data evidence | +|---------|--------|--------------------| +| `` wrapper root rejected | dataverse-xml-lsp | 295 / 712 files fail root detection | +| `` root rejected | dataverse-xml-lsp | modern per-app sitemaps uncovered | +| Nested-action errors not caught | cloud-flow-json-lsp | bogus `runAfter` in a nested action passes silently | + +Two design facts established during investigation drive the approach: + +- `AppModuleSiteMap` contains an inner `` element carrying the Area/Group/SubArea + tree. Extracting and validating that inner element against `SiteMap.xsd` produces **0 + errors** on real data — so this fix is authoritative, not indicative. +- A real `` fragment carries `layoutxml` / `LocalizedNames` (absent from the + bundled `Fetch.xsd`) and omits elements the schema marks required. Validation is + therefore **indicative** — the same situation as FormXml today. + +## Goals + +- Real exported saved queries and per-app sitemaps validate instead of being rejected at + root detection. +- Cloud-flow structural validation reaches actions nested inside Scope / If / Foreach / + Switch, not just top-level actions. +- Every fix ships with a regression test derived from the real-export shapes. + +## Non-goals + +- Making saved-query validation authoritative (patching the Microsoft XSD) — deferred to + the roadmap. +- Semantic flow linting (condition rows, connection-reference resolution, etc.) — deferred + to the roadmap. +- The minor "directory input is shallow / flags Entity.xml" polish — out of scope. + +## Design + +All three changes are small; two reuse existing machinery. + +### Fix A — `dataverse-xml-lsp`: `` wrapper root + +Root cause: `Validate-DataverseXml.ps1` maps the singular inner element `savedquery`, but +pac exports the file with a `` wrapper, so root detection reports +`Unknown root element `. + +Changes: + +- `scripts/Validate-DataverseXml.ps1`: add `innerElementByRoot['savedqueries'] = + 'savedquery'`. This reuses the existing per-fragment extraction loop (identical code path + to `forms` -> `systemform/form`); no new logic. Each `` child is validated + against `Fetch.xsd`. +- `hooks/validate-wrapper.mjs`: add `savedqueries` to `OWNED_ROOTS` so the PostToolUse hook + runs the CLI validator after an edit. +- `scripts/lsp-launch.mjs`: remove the stale live association `**/SavedQueries/**/*.xml -> + Fetch.xsd`. It mis-fires: lemminx validates the whole document, whose root + `` is not declared in `Fetch.xsd`, producing a misleading root-level error. + Coverage moves to the hook, consistent with the other wrapper/lag-prone roots. + +Behaviour: **indicative**. The known OOB errors (`layoutxml` not declared; required +`name` / `returnedtypecode` reported missing) are expected noise, read the same way as +FormXml — confirm your own edit is not named in the output. + +### Fix B — `dataverse-xml-lsp`: `` wrapper root + +Root cause: model-driven-app sitemaps export with root ``, which is not +in the root map; the `**/SiteMap*.xml` glob does not match the filename either. + +Changes: + +- `scripts/Validate-DataverseXml.ps1`: add `rootToSchema['AppModuleSiteMap'] = + 'SiteMap.xsd'` and `innerElementByRoot['AppModuleSiteMap'] = 'SiteMap'`. The inner + `` subtree is extracted and validated. +- `hooks/validate-wrapper.mjs`: add `AppModuleSiteMap` to `OWNED_ROOTS`. + +Behaviour: **authoritative** (0 errors on real data). No live LSP association — lemminx +cannot do per-fragment extraction, so this follows the wrapper-root-via-hook pattern. + +### Fix C — `cloud-flow-json-lsp`: recursive nested-action validation + +Root cause: `schemas/cloud-flow-clientdata.schema.json` constrains only the top-level +`actions` map. Actions nested inside a container (`Scope` / `If` / `Foreach` / `Switch`) +fall under `additionalProperties: true` and are unchecked, so a bogus nested `runAfter` +status or a missing nested `type` passes silently. + +Changes: + +- Hoist the action object shape into `definitions/action` (draft-07 keyword `definitions`, + not `$defs`, so the bundled and VS Code validators both resolve it). +- `$ref` `#/definitions/action` from `definition.actions.additionalProperties` and, + recursively, from the container keys inside an action: `actions`, `else.actions`, + `cases.*.actions`, `default.actions`. +- Keep `inputs` untyped (the intentional looseness that avoids OpenApiConnection false + positives). Only `type` presence and the `runAfter` status enum are enforced, now at any + depth. + +## Testing + +Each plugin gains fixtures derived from the real-export shapes, kept minimal: + +- dataverse-xml-lsp: a valid `` file (passes with only the documented OOB + noise), a valid `` file (passes clean), and an invalid variant of each. +- cloud-flow-json-lsp: a valid nested-action flow and an invalid one with a bogus + `runAfter` status on a nested action — which must now be caught. + +Assertions extend the existing suites: + +- `dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1`: new roots resolve to the right + schema and inner element; valid/invalid fixtures behave as expected. +- `dataverse-xml-lsp/tests/Hook.Tests.ps1`: the hook now claims `savedqueries` and + `AppModuleSiteMap`. +- `dataverse-xml-lsp` LSP association test: the `SavedQueries` association is gone. +- `cloud-flow-json-lsp`: the setup self-check / smoke test gains the nested-bad-runAfter + case and confirms a diagnostic fires. + +Committed fixtures stay minimal and synthetic (derived from the real shapes, not client +data). Two real client exports — `Claude/QEII` and `Claude/NZLS` — are available locally as +additional corpora for a broad pre-release sweep of each fix; they are not committed. + +## Docs and versioning + +- `dataverse-xml-lsp/docs/guide.md`: add both new roots to the root->schema table; extend + the "indicative, not authoritative" note to name `savedqueries`. +- `CHANGELOG.md`: one entry per plugin. +- Version bumps: `dataverse-xml-lsp` 2.1.0 -> 2.2.0; `cloud-flow-json-lsp` 2.0.0 -> 2.1.0. + +## Roadmap (documented, not built here) + +1. **Authoritative saved queries.** Add a post-download patch step in + `dataverse-xml-lsp/scripts/Get-Schemas.ps1` that extends the stock `Fetch.xsd` (declare + `layoutxml` / `LocalizedNames`, relax the required attributes) so saved queries validate + cleanly rather than indicatively, and the patch survives XSD re-download. +2. **Flow semantic-validate skill.** Port the checks from the personal + `power-automate-flow-dev` skill (`flow-lint.ps1` + `flow-validate-conditions.ps1`) into + `cloud-flow-json-lsp` as a bundled semantic layer above the shape schema: `runAfter` + integrity and cycles, `connectionName` -> `connectionReferences` resolution, + child-invoker connection detection, hardcoded env GUID/host/site-URL warnings, and + condition-row checks (empty and/or groups, empty operands, the always-true + `{"equals":["",""]}` row, stringified literals). This is the "Semantics" layer the guide + already references as living in an external skill. From 871a9d50b22d244e36c25fdecd079f132e29faf0 Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Wed, 22 Jul 2026 12:20:36 +1200 Subject: [PATCH 2/7] cloud-flow-json-lsp: validate nested actions recursively (runAfter enum + type at any depth) --- plugins/cloud-flow-json-lsp/docs/guide.md | 2 +- .../schemas/cloud-flow-clientdata.schema.json | 45 ++++++++++--------- .../tests/Schema.Tests.ps1 | 32 +++++++++++++ .../invalid/nested-bad-runafter-status.json | 23 ++++++++++ .../tests/fixtures/valid/nested-actions.json | 23 ++++++++++ 5 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 create mode 100644 plugins/cloud-flow-json-lsp/tests/fixtures/invalid/nested-bad-runafter-status.json create mode 100644 plugins/cloud-flow-json-lsp/tests/fixtures/valid/nested-actions.json diff --git a/plugins/cloud-flow-json-lsp/docs/guide.md b/plugins/cloud-flow-json-lsp/docs/guide.md index 65ebc66..1a36800 100644 --- a/plugins/cloud-flow-json-lsp/docs/guide.md +++ b/plugins/cloud-flow-json-lsp/docs/guide.md @@ -13,7 +13,7 @@ It is one layer of a two-layer story — do not confuse them: | Layer | What it checks | Where | |-------|----------------|-------| -| **Shape (this plugin)** | JSON well-formedness + the clientdata/WDL wrapper structure: `properties.definition` present, `definition` has `$schema`/`triggers`/`actions`, `runAfter` statuses are the WDL enum. Live, in-editor. | `schemas/cloud-flow-clientdata.schema.json` via `vscode-json-language-server` (`.lsp.json`) | +| **Shape (this plugin)** | JSON well-formedness + the clientdata/WDL wrapper structure: `properties.definition` present, `definition` has `$schema`/`triggers`/`actions`, `runAfter` statuses are the WDL enum, validated recursively through Scope/If/Foreach/Switch. Live, in-editor. | `schemas/cloud-flow-clientdata.schema.json` via `vscode-json-language-server` (`.lsp.json`) | | **Semantics (the `power-automate-flow-dev` skill)** | Cross-node rules JSON Schema *cannot* express: `runAfter` naming a real sibling, `connectionName` resolving to a declared connection reference, child-invoker connections, hard-coded environment values, condition rows. Plus the export → unpack → edit → pack → import → verify round-trip. | that skill's `flow-lint.ps1` | **Rule of thumb:** the LSP catches "this isn't a well-formed flow file." `flow-lint.ps1` catches "this diff --git a/plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json b/plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json index 5e7cae3..736d0c2 100644 --- a/plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json +++ b/plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json @@ -69,28 +69,8 @@ }, "actions": { "type": "object", - "description": "The action graph. Order is expressed via runAfter, NOT array position. Each action has a `type` and usually `inputs` + `runAfter`.", - "additionalProperties": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "description": "e.g. OpenApiConnection, Compose, Scope, If, Foreach, InitializeVariable, SetVariable, Http, Response, Terminate, Switch." - }, - "inputs": { "description": "Action inputs. Type varies by action — e.g. Compose/Set/Append inputs can be a string, number, array or object — so left untyped to avoid false positives." }, - "runAfter": { - "type": "object", - "description": "Map of upstream-action-name -> array of statuses to run after (Succeeded / Failed / Skipped / TimedOut). Empty {} = runs first.", - "additionalProperties": { - "type": "array", - "items": { "enum": ["Succeeded", "Failed", "Skipped", "TimedOut"] } - } - }, - "runtimeConfiguration": { "type": "object", "additionalProperties": true } - }, - "additionalProperties": true - } + "description": "The action graph. Order is expressed via runAfter, NOT array position. Nested actions (Scope/If/Foreach/Switch) are validated recursively.", + "additionalProperties": { "$ref": "#/definitions/action" } }, "outputs": { "type": "object", "additionalProperties": true } } @@ -98,5 +78,26 @@ } }, "schemaVersion": { "type": "string" } + }, + "definitions": { + "action": { + "type": "object", + "required": ["type"], + "additionalProperties": true, + "properties": { + "type": { "type": "string", "description": "e.g. OpenApiConnection, Compose, Scope, If, Foreach, InitializeVariable, SetVariable, Http, Response, Terminate, Switch." }, + "inputs": { "description": "Action inputs. Type varies by action, so left untyped to avoid false positives." }, + "runAfter": { + "type": "object", + "description": "Map of upstream-action-name -> array of statuses to run after (Succeeded / Failed / Skipped / TimedOut). Empty {} = runs first.", + "additionalProperties": { "type": "array", "items": { "enum": ["Succeeded", "Failed", "Skipped", "TimedOut"] } } + }, + "runtimeConfiguration": { "type": "object", "additionalProperties": true }, + "actions": { "type": "object", "additionalProperties": { "$ref": "#/definitions/action" } }, + "else": { "type": "object", "properties": { "actions": { "type": "object", "additionalProperties": { "$ref": "#/definitions/action" } } } }, + "default": { "type": "object", "properties": { "actions": { "type": "object", "additionalProperties": { "$ref": "#/definitions/action" } } } }, + "cases": { "type": "object", "additionalProperties": { "type": "object", "properties": { "actions": { "type": "object", "additionalProperties": { "$ref": "#/definitions/action" } } } } } + } + } } } diff --git a/plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 new file mode 100644 index 0000000..fd80bab --- /dev/null +++ b/plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 @@ -0,0 +1,32 @@ +#requires -Version 7 +# Headless (node-independent) schema regression via Test-Json. Every fixture under +# tests/fixtures/{valid,invalid} must match/violate the bundled clientdata schema. + +Describe 'cloud-flow clientdata schema (Test-Json)' { + BeforeAll { + $script:pluginRoot = Split-Path $PSScriptRoot -Parent + $script:schema = Join-Path $pluginRoot 'schemas' 'cloud-flow-clientdata.schema.json' + $script:fixtures = Join-Path $PSScriptRoot 'fixtures' + + function Test-Fixture([string]$Path) { + try { Get-Content $Path -Raw | Test-Json -SchemaFile $script:schema -ErrorAction Stop; return $true } + catch { return $false } + } + } + + It 'passes every valid fixture' { + foreach ($f in Get-ChildItem (Join-Path $fixtures 'valid') -Filter *.json) { + (Test-Fixture $f.FullName) | Should -BeTrue -Because "$($f.Name) should be schema-valid" + } + } + + It 'fails every invalid fixture' { + foreach ($f in Get-ChildItem (Join-Path $fixtures 'invalid') -Filter *.json) { + (Test-Fixture $f.FullName) | Should -BeFalse -Because "$($f.Name) should violate the schema" + } + } + + It 'rejects a bogus runAfter status on a NESTED action specifically' { + (Test-Fixture (Join-Path $fixtures 'invalid' 'nested-bad-runafter-status.json')) | Should -BeFalse + } +} diff --git a/plugins/cloud-flow-json-lsp/tests/fixtures/invalid/nested-bad-runafter-status.json b/plugins/cloud-flow-json-lsp/tests/fixtures/invalid/nested-bad-runafter-status.json new file mode 100644 index 0000000..242eeef --- /dev/null +++ b/plugins/cloud-flow-json-lsp/tests/fixtures/invalid/nested-bad-runafter-status.json @@ -0,0 +1,23 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "triggers": { "Recurrence": { "type": "Recurrence" } }, + "actions": { + "For_each_item": { + "type": "Foreach", + "foreach": "@triggerOutputs()", + "runAfter": {}, + "actions": { + "Set_inner": { + "type": "SetVariable", + "inputs": { "name": "v", "value": "x" }, + "runAfter": { "Prev": ["TotallyBogusStatus"] } + } + } + } + } + } + } +} diff --git a/plugins/cloud-flow-json-lsp/tests/fixtures/valid/nested-actions.json b/plugins/cloud-flow-json-lsp/tests/fixtures/valid/nested-actions.json new file mode 100644 index 0000000..1661449 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/tests/fixtures/valid/nested-actions.json @@ -0,0 +1,23 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "triggers": { "Recurrence": { "type": "Recurrence" } }, + "actions": { + "For_each_item": { + "type": "Foreach", + "foreach": "@triggerOutputs()", + "runAfter": {}, + "actions": { + "Set_inner": { + "type": "SetVariable", + "inputs": { "name": "v", "value": "x" }, + "runAfter": { "Prev": ["Succeeded"] } + } + } + } + } + } + } +} From 8908b53c7a20c9ea53049f955f14160e84bd2f3a Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Wed, 22 Jul 2026 12:22:00 +1200 Subject: [PATCH 3/7] dataverse-xml-lsp: validate wrapper root via inner savedquery extraction --- plugins/dataverse-xml-lsp/docs/guide.md | 11 ++++++----- .../hooks/validate-wrapper.mjs | 1 + .../scripts/Set-LspSchemaPaths.ps1 | 12 ++++++------ .../scripts/Validate-DataverseXml.ps1 | 2 ++ .../dataverse-xml-lsp/scripts/lsp-launch.mjs | 10 +++++----- plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 | 1 + .../tests/LspAssociations.Tests.ps1 | 18 ++++++++++++++++++ .../tests/Validate-DataverseXml.Tests.ps1 | 10 +++++----- .../fixtures/invalid/savedqueries-wrapper.xml | 5 +++++ .../fixtures/valid/savedqueries-wrapper.xml | 8 ++++++++ 10 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 plugins/dataverse-xml-lsp/tests/fixtures/invalid/savedqueries-wrapper.xml create mode 100644 plugins/dataverse-xml-lsp/tests/fixtures/valid/savedqueries-wrapper.xml diff --git a/plugins/dataverse-xml-lsp/docs/guide.md b/plugins/dataverse-xml-lsp/docs/guide.md index eb81a43..44817f4 100644 --- a/plugins/dataverse-xml-lsp/docs/guide.md +++ b/plugins/dataverse-xml-lsp/docs/guide.md @@ -34,7 +34,7 @@ It picks the schema by the file's **root element**: | `SiteMap` | `SiteMap.xsd` | App navigation — authoritative | | `form` / `forms` | `FormXml.xsd` | Forms; pac's `` wrapper → each inner `systemform/form` validated — indicative (schema lags modern form attrs) | | `fetch` | `Fetch.xsd` | FetchXML queries (`.fetchxml` files, query fragments) — authoritative | -| `savedquery` | `Fetch.xsd` | pac `SavedQueries/*.xml` — authoritative | +| `savedquery` / `savedqueries` | `Fetch.xsd` | pac `SavedQueries/*.xml`; `` wrapper → each inner `savedquery` validated — indicative (schema lags `layoutxml`/`LocalizedNames`) | | `visualization` / `datadefinition` | `VisualizationDataDescription.xsd` | Charts; the `` wrapper's inner `datadescription/datadefinition` is validated (escaped inner XML handled — see gotchas) | | `configuration` | `isv.config.xsd` | Legacy ISV config | | `importexportxml` (lowercase) | `ParameterXml.xsd` | Configuration-migration parameter XML — note the case difference from `ImportExportXml` | @@ -104,10 +104,11 @@ pack/import mechanics. ## Gotchas (learned building this) -- **Whole-file `Customizations.xml` AND whole-form `FormXml` validation are indicative, not - authoritative.** The bundled schema is `9.0.0.2090`; modern Dataverse exports/forms include - newer attributes/elements it doesn't declare (`OrganizationVersion`, `CanvasApps`, empty - `AppModules`; on forms `headerdensity`, `contenttype`, `UClientRecordSourcesJSON`, …), so it +- **Whole-file `Customizations.xml`, whole-form `FormXml`, AND saved-query validation are + indicative, not authoritative.** The bundled schema is `9.0.0.2090`; modern Dataverse + exports/forms include newer attributes/elements it doesn't declare (`OrganizationVersion`, + `CanvasApps`, empty `AppModules`; on forms `headerdensity`, `contenttype`, + `UClientRecordSourcesJSON`; on saved queries `layoutxml`, `LocalizedNames`, …), so it reports false "not declared" errors. Confirm your OWN edits are clean by checking no error references them (grep the output for your element/attribute names), and treat pre-existing OOB noise as expected. The **ribbon** fragment (`RibbonCore.xsd`) is stable and fully authoritative. diff --git a/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs b/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs index b74ea08..760928d 100644 --- a/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs +++ b/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs @@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url'; // (ParameterXml) is distinct from 'ImportExportXml' (customizations.xml, which the LSP owns). const OWNED_ROOTS = new Set([ 'form', 'forms', 'datadefinition', 'visualization', 'viewers', 'importexportxml', + 'savedqueries', ]); function rootElement(file) { diff --git a/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 b/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 index 0053503..39cc9e1 100644 --- a/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 +++ b/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 @@ -38,16 +38,16 @@ if (-not $SchemaVersion) { } $schemaDir = Join-Path $pluginRoot 'schemas' $SchemaVersion -# pattern -> schema filename. Charts () AND forms () have no association on -# purpose: they are pac WRAPPER files whose root the XSD doesn't declare, so whole-file validation -# would only produce a false error on the wrapper root. The validator (Validate-DataverseXml.ps1) -# owns them via per-fragment extraction (systemform/form, datadescription/datadefinition). Kept in -# sync with the associations the shim (scripts/lsp-launch.mjs) injects for Claude Code. +# pattern -> schema filename. Charts (), forms () AND saved queries +# () have no association on purpose: they are pac WRAPPER files whose root the XSD +# doesn't declare, so whole-file validation would only produce a false error on the wrapper root. +# The validator (Validate-DataverseXml.ps1) owns them via per-fragment extraction (systemform/form, +# datadescription/datadefinition, savedquery). Kept in sync with the associations the shim +# (scripts/lsp-launch.mjs) injects for Claude Code. $assoc = [ordered]@{ '**/RibbonDiff.xml' = 'RibbonCore.xsd' '**/[Cc]ustomizations.xml' = 'CustomizationsSolution.xsd' '**/SiteMap*.xml' = 'SiteMap.xsd' - '**/SavedQueries/**/*.xml' = 'Fetch.xsd' '**/*.fetchxml' = 'Fetch.xsd' '**/isv.config.xml' = 'isv.config.xsd' } diff --git a/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 b/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 index f459b14..d6dccc9 100644 --- a/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 +++ b/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 @@ -69,6 +69,7 @@ $rootToSchema['form'] = 'FormXml.xsd' $rootToSchema['forms'] = 'FormXml.xsd' $rootToSchema['fetch'] = 'Fetch.xsd' $rootToSchema['savedquery'] = 'Fetch.xsd' +$rootToSchema['savedqueries'] = 'Fetch.xsd' $rootToSchema['datadefinition'] = 'VisualizationDataDescription.xsd' $rootToSchema['visualization'] = 'VisualizationDataDescription.xsd' $rootToSchema['configuration'] = 'isv.config.xsd' @@ -80,6 +81,7 @@ $rootToSchema['viewers'] = 'reports.config.xsd' $innerElementByRoot = [System.Collections.Generic.Dictionary[string, string]]::new() $innerElementByRoot['forms'] = 'systemform/form' $innerElementByRoot['visualization'] = 'datadescription/datadefinition' +$innerElementByRoot['savedqueries'] = 'savedquery' # Cache compiled schema sets by xsd filename so a batch of files loads each schema once. $schemaSetCache = @{} diff --git a/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs b/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs index 2cd1102..47b4c9a 100644 --- a/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs +++ b/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs @@ -34,15 +34,15 @@ const bins = readdirSync(binDir).filter((f) => f.startsWith('lemminx')); if (bins.length !== 1) throw new Error(`lsp-launch: expected exactly one lemminx binary in ${binDir}, found ${bins.length}. Run scripts/Get-Lemminx.ps1.`); const serverExe = join(binDir, bins[0]); -// pattern -> XSD filename. Charts () AND forms () have no association on -// purpose: they are pac WRAPPER files whose root the XSD doesn't declare, so lemminx would -// false-positive on the wrapper root. The validator (Validate-DataverseXml.ps1) owns them, via -// per-fragment extraction (systemform/form, datadescription/datadefinition). +// pattern -> XSD filename. Charts (), forms () AND saved queries +// () have no association on purpose: they are pac WRAPPER files whose root the XSD +// doesn't declare, so lemminx would false-positive on the wrapper root. The validator +// (Validate-DataverseXml.ps1) owns them, via per-fragment extraction (systemform/form, +// datadescription/datadefinition, savedquery). const assoc = { '**/RibbonDiff.xml': 'RibbonCore.xsd', '**/[Cc]ustomizations.xml': 'CustomizationsSolution.xsd', '**/SiteMap*.xml': 'SiteMap.xsd', - '**/SavedQueries/**/*.xml': 'Fetch.xsd', '**/*.fetchxml': 'Fetch.xsd', '**/isv.config.xml': 'isv.config.xsd', }; diff --git a/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 index a3d5d47..81a650f 100644 --- a/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 +++ b/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 @@ -16,6 +16,7 @@ Describe 'wrapper hook gate (validate-wrapper.mjs)' { @{ Root = 'visualization'; File = 'visualization-wrapper.xml' } @{ Root = 'viewers'; File = 'viewers.xml' } @{ Root = 'importexportxml'; File = 'parameterxml.xml' } + @{ Root = 'savedqueries'; File = 'savedqueries-wrapper.xml' } ) } diff --git a/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 index c753460..0962b9d 100644 --- a/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 +++ b/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 @@ -40,6 +40,7 @@ Describe 'XML LSP is not associated to validator-owned wrapper roots' { # Representative pac-unpacked wrapper file paths (root / ). $script:formsWrapper = 'src/Entities/account/FormXml/main/00000000-0000-0000-0000-000000000000.xml' + $script:savedQueryWrapper = 'src/Entities/account/SavedQueries/00000000-0000-0000-0000-000000000000.xml' } It 'parses a non-empty association map from the shim' { @@ -58,6 +59,14 @@ Describe 'XML LSP is not associated to validator-owned wrapper roots' { } $hit | Should -BeNullOrEmpty -Because "no LSP association may match a pac wrapper file ($formsWrapper); lemminx would false-positive on the root. Matched: $($hit -join ', ')" } + + It 'does not associate pac SavedQueries wrapper files (validator-owned)' { + $hit = @() + foreach ($glob in $assoc.Keys) { + if (& $matcher $glob $savedQueryWrapper) { $hit += "$glob -> $($assoc[$glob])" } + } + $hit | Should -BeNullOrEmpty -Because "no LSP association may match a pac wrapper file ($savedQueryWrapper); lemminx would false-positive on the root. Matched: $($hit -join ', ')" + } } Describe 'VS Code association map is not associated to validator-owned wrapper roots' { @@ -99,6 +108,7 @@ Describe 'VS Code association map is not associated to validator-owned wrapper r } $script:matcher = ${function:Test-GlobMatch} $script:formsWrapper = 'src/Entities/account/FormXml/main/00000000-0000-0000-0000-000000000000.xml' + $script:savedQueryWrapper = 'src/Entities/account/SavedQueries/00000000-0000-0000-0000-000000000000.xml' } It 'parses a non-empty association map from Set-LspSchemaPaths.ps1' { @@ -116,4 +126,12 @@ Describe 'VS Code association map is not associated to validator-owned wrapper r } $hit | Should -BeNullOrEmpty -Because "no VS Code association may match a pac wrapper file ($formsWrapper); the RedHat XML extension would false-positive on the root. Matched: $($hit -join ', ')" } + + It 'does not associate pac SavedQueries wrapper files (validator-owned)' { + $hit = @() + foreach ($glob in $vsAssoc.Keys) { + if (& $matcher $glob $savedQueryWrapper) { $hit += "$glob -> $($vsAssoc[$glob])" } + } + $hit | Should -BeNullOrEmpty -Because "no VS Code association may match a pac wrapper file ($savedQueryWrapper); the RedHat XML extension would false-positive on the root. Matched: $($hit -join ', ')" + } } diff --git a/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 index e4cf9b7..bc91b00 100644 --- a/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 +++ b/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 @@ -4,14 +4,14 @@ BeforeDiscovery { $script:validValidatedByRoot = @( 'ribbon.xml', 'sitemap.xml', 'form.xml', 'forms-wrapper.xml', 'fetch.xml', - 'savedquery.xml', 'datadefinition.xml', 'visualization-wrapper.xml', - 'visualization-escaped.xml', 'isvconfig.xml', 'parameterxml.xml', - 'viewers.xml', 'importexport.xml' + 'savedquery.xml', 'savedqueries-wrapper.xml', 'datadefinition.xml', + 'visualization-wrapper.xml', 'visualization-escaped.xml', 'isvconfig.xml', + 'parameterxml.xml', 'viewers.xml', 'importexport.xml' ) $script:invalidFixtures = @( 'ribbon.xml', 'sitemap.xml', 'form.xml', 'forms-wrapper.xml', 'fetch.xml', - 'savedquery.xml', 'datadefinition.xml', 'isvconfig.xml', 'parameterxml.xml', - 'viewers.xml', 'importexport.xml' + 'savedquery.xml', 'savedqueries-wrapper.xml', 'datadefinition.xml', + 'isvconfig.xml', 'parameterxml.xml', 'viewers.xml', 'importexport.xml' ) } diff --git a/plugins/dataverse-xml-lsp/tests/fixtures/invalid/savedqueries-wrapper.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/savedqueries-wrapper.xml new file mode 100644 index 0000000..a279915 --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/savedqueries-wrapper.xml @@ -0,0 +1,5 @@ + + + 1 + + diff --git a/plugins/dataverse-xml-lsp/tests/fixtures/valid/savedqueries-wrapper.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/savedqueries-wrapper.xml new file mode 100644 index 0000000..7117629 --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/fixtures/valid/savedqueries-wrapper.xml @@ -0,0 +1,8 @@ + + + Active Accounts + {00000000-0000-0000-0000-000000000001} + 1 + 0 + + From f3aba86c5f730d597e87238db7c4bbc8af5e19a7 Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Wed, 22 Jul 2026 12:28:02 +1200 Subject: [PATCH 4/7] cloud-flow-json-lsp: fold nested-action guard into existing schema test --- .../tests/LspConfig.Tests.ps1 | 7 ++++ .../tests/Schema.Tests.ps1 | 32 ------------------- 2 files changed, 7 insertions(+), 32 deletions(-) delete mode 100644 plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 diff --git a/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 index f183535..ce29298 100644 --- a/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 +++ b/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 @@ -66,4 +66,11 @@ Describe 'bundled schema distinguishes valid from invalid fixtures' { $files.Count | Should -BeGreaterThan 0 foreach ($f in $files) { Test-Fixture $f.FullName | Should -BeFalse -Because "$($f.Name) should be rejected" } } + + It 'rejects a bogus runAfter status on a nested action' { + # nested-bad-runafter-status.json puts the bad status on an action inside a Foreach; + # a non-recursive schema would miss it. + $bad = Join-Path $invalidDir 'nested-bad-runafter-status.json' + [bool](Get-Content $bad -Raw | Test-Json -SchemaFile $schema) | Should -BeFalse + } } diff --git a/plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 deleted file mode 100644 index fd80bab..0000000 --- a/plugins/cloud-flow-json-lsp/tests/Schema.Tests.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -#requires -Version 7 -# Headless (node-independent) schema regression via Test-Json. Every fixture under -# tests/fixtures/{valid,invalid} must match/violate the bundled clientdata schema. - -Describe 'cloud-flow clientdata schema (Test-Json)' { - BeforeAll { - $script:pluginRoot = Split-Path $PSScriptRoot -Parent - $script:schema = Join-Path $pluginRoot 'schemas' 'cloud-flow-clientdata.schema.json' - $script:fixtures = Join-Path $PSScriptRoot 'fixtures' - - function Test-Fixture([string]$Path) { - try { Get-Content $Path -Raw | Test-Json -SchemaFile $script:schema -ErrorAction Stop; return $true } - catch { return $false } - } - } - - It 'passes every valid fixture' { - foreach ($f in Get-ChildItem (Join-Path $fixtures 'valid') -Filter *.json) { - (Test-Fixture $f.FullName) | Should -BeTrue -Because "$($f.Name) should be schema-valid" - } - } - - It 'fails every invalid fixture' { - foreach ($f in Get-ChildItem (Join-Path $fixtures 'invalid') -Filter *.json) { - (Test-Fixture $f.FullName) | Should -BeFalse -Because "$($f.Name) should violate the schema" - } - } - - It 'rejects a bogus runAfter status on a NESTED action specifically' { - (Test-Fixture (Join-Path $fixtures 'invalid' 'nested-bad-runafter-status.json')) | Should -BeFalse - } -} From e287b27107077583f4e32fa4f767cc2c88f2df6e Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Wed, 22 Jul 2026 12:32:43 +1200 Subject: [PATCH 5/7] dataverse-xml-lsp: validate via inner SiteMap extraction --- plugins/dataverse-xml-lsp/docs/guide.md | 1 + .../hooks/validate-wrapper.mjs | 2 +- .../scripts/Validate-DataverseXml.ps1 | 2 ++ plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 | 1 + .../tests/LspAssociations.Tests.ps1 | 18 ++++++++++++++++++ .../tests/Validate-DataverseXml.Tests.ps1 | 5 +++-- .../invalid/appmodulesitemap-wrapper.xml | 6 ++++++ .../valid/appmodulesitemap-wrapper.xml | 6 ++++++ 8 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 plugins/dataverse-xml-lsp/tests/fixtures/invalid/appmodulesitemap-wrapper.xml create mode 100644 plugins/dataverse-xml-lsp/tests/fixtures/valid/appmodulesitemap-wrapper.xml diff --git a/plugins/dataverse-xml-lsp/docs/guide.md b/plugins/dataverse-xml-lsp/docs/guide.md index 44817f4..eba94b3 100644 --- a/plugins/dataverse-xml-lsp/docs/guide.md +++ b/plugins/dataverse-xml-lsp/docs/guide.md @@ -32,6 +32,7 @@ It picks the schema by the file's **root element**: |---------------------|-------------------------------|-------| | `RibbonDiffXml` | `RibbonCore.xsd` | Per-entity `RibbonDiff.xml` or app ribbon — **authoritative** | | `SiteMap` | `SiteMap.xsd` | App navigation — authoritative | +| `AppModuleSiteMap` | `SiteMap.xsd` | Model-driven-app sitemap; wrapper → inner `SiteMap` validated — authoritative | | `form` / `forms` | `FormXml.xsd` | Forms; pac's `` wrapper → each inner `systemform/form` validated — indicative (schema lags modern form attrs) | | `fetch` | `Fetch.xsd` | FetchXML queries (`.fetchxml` files, query fragments) — authoritative | | `savedquery` / `savedqueries` | `Fetch.xsd` | pac `SavedQueries/*.xml`; `` wrapper → each inner `savedquery` validated — indicative (schema lags `layoutxml`/`LocalizedNames`) | diff --git a/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs b/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs index 760928d..16e8a1a 100644 --- a/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs +++ b/plugins/dataverse-xml-lsp/hooks/validate-wrapper.mjs @@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url'; // (ParameterXml) is distinct from 'ImportExportXml' (customizations.xml, which the LSP owns). const OWNED_ROOTS = new Set([ 'form', 'forms', 'datadefinition', 'visualization', 'viewers', 'importexportxml', - 'savedqueries', + 'savedqueries', 'AppModuleSiteMap', ]); function rootElement(file) { diff --git a/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 b/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 index d6dccc9..5995a5a 100644 --- a/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 +++ b/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 @@ -65,6 +65,7 @@ $rootToSchema['ImportExportXml'] = 'CustomizationsSolution.xsd' $rootToSchema['importexportxml'] = 'ParameterXml.xsd' $rootToSchema['RibbonDiffXml'] = 'RibbonCore.xsd' $rootToSchema['SiteMap'] = 'SiteMap.xsd' +$rootToSchema['AppModuleSiteMap'] = 'SiteMap.xsd' $rootToSchema['form'] = 'FormXml.xsd' $rootToSchema['forms'] = 'FormXml.xsd' $rootToSchema['fetch'] = 'Fetch.xsd' @@ -82,6 +83,7 @@ $innerElementByRoot = [System.Collections.Generic.Dictionary[string, string]]::n $innerElementByRoot['forms'] = 'systemform/form' $innerElementByRoot['visualization'] = 'datadescription/datadefinition' $innerElementByRoot['savedqueries'] = 'savedquery' +$innerElementByRoot['AppModuleSiteMap'] = 'SiteMap' # Cache compiled schema sets by xsd filename so a batch of files loads each schema once. $schemaSetCache = @{} diff --git a/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 index 81a650f..d468cd0 100644 --- a/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 +++ b/plugins/dataverse-xml-lsp/tests/Hook.Tests.ps1 @@ -17,6 +17,7 @@ Describe 'wrapper hook gate (validate-wrapper.mjs)' { @{ Root = 'viewers'; File = 'viewers.xml' } @{ Root = 'importexportxml'; File = 'parameterxml.xml' } @{ Root = 'savedqueries'; File = 'savedqueries-wrapper.xml' } + @{ Root = 'AppModuleSiteMap'; File = 'appmodulesitemap-wrapper.xml' } ) } diff --git a/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 index 0962b9d..ae850f8 100644 --- a/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 +++ b/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 @@ -41,6 +41,7 @@ Describe 'XML LSP is not associated to validator-owned wrapper roots' { # Representative pac-unpacked wrapper file paths (root / ). $script:formsWrapper = 'src/Entities/account/FormXml/main/00000000-0000-0000-0000-000000000000.xml' $script:savedQueryWrapper = 'src/Entities/account/SavedQueries/00000000-0000-0000-0000-000000000000.xml' + $script:appModuleSiteMap = 'src/AppModuleSiteMaps/test_app/AppModuleSiteMap.xml' } It 'parses a non-empty association map from the shim' { @@ -67,6 +68,14 @@ Describe 'XML LSP is not associated to validator-owned wrapper roots' { } $hit | Should -BeNullOrEmpty -Because "no LSP association may match a pac wrapper file ($savedQueryWrapper); lemminx would false-positive on the root. Matched: $($hit -join ', ')" } + + It 'does not associate pac AppModuleSiteMap wrapper files (validator-owned)' { + $hit = @() + foreach ($glob in $assoc.Keys) { + if (& $matcher $glob $appModuleSiteMap) { $hit += "$glob -> $($assoc[$glob])" } + } + $hit | Should -BeNullOrEmpty -Because "the SiteMap*.xml glob must not match a pac wrapper file ($appModuleSiteMap); lemminx would false-positive on the root. Matched: $($hit -join ', ')" + } } Describe 'VS Code association map is not associated to validator-owned wrapper roots' { @@ -109,6 +118,7 @@ Describe 'VS Code association map is not associated to validator-owned wrapper r $script:matcher = ${function:Test-GlobMatch} $script:formsWrapper = 'src/Entities/account/FormXml/main/00000000-0000-0000-0000-000000000000.xml' $script:savedQueryWrapper = 'src/Entities/account/SavedQueries/00000000-0000-0000-0000-000000000000.xml' + $script:appModuleSiteMap = 'src/AppModuleSiteMaps/test_app/AppModuleSiteMap.xml' } It 'parses a non-empty association map from Set-LspSchemaPaths.ps1' { @@ -134,4 +144,12 @@ Describe 'VS Code association map is not associated to validator-owned wrapper r } $hit | Should -BeNullOrEmpty -Because "no VS Code association may match a pac wrapper file ($savedQueryWrapper); the RedHat XML extension would false-positive on the root. Matched: $($hit -join ', ')" } + + It 'does not associate pac AppModuleSiteMap wrapper files (validator-owned)' { + $hit = @() + foreach ($glob in $vsAssoc.Keys) { + if (& $matcher $glob $appModuleSiteMap) { $hit += "$glob -> $($vsAssoc[$glob])" } + } + $hit | Should -BeNullOrEmpty -Because "the SiteMap*.xml glob must not match a pac wrapper file ($appModuleSiteMap); lemminx would false-positive on the root. Matched: $($hit -join ', ')" + } } diff --git a/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 index bc91b00..18dc7ab 100644 --- a/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 +++ b/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 @@ -6,12 +6,13 @@ BeforeDiscovery { 'ribbon.xml', 'sitemap.xml', 'form.xml', 'forms-wrapper.xml', 'fetch.xml', 'savedquery.xml', 'savedqueries-wrapper.xml', 'datadefinition.xml', 'visualization-wrapper.xml', 'visualization-escaped.xml', 'isvconfig.xml', - 'parameterxml.xml', 'viewers.xml', 'importexport.xml' + 'parameterxml.xml', 'viewers.xml', 'importexport.xml', 'appmodulesitemap-wrapper.xml' ) $script:invalidFixtures = @( 'ribbon.xml', 'sitemap.xml', 'form.xml', 'forms-wrapper.xml', 'fetch.xml', 'savedquery.xml', 'savedqueries-wrapper.xml', 'datadefinition.xml', - 'isvconfig.xml', 'parameterxml.xml', 'viewers.xml', 'importexport.xml' + 'isvconfig.xml', 'parameterxml.xml', 'viewers.xml', 'importexport.xml', + 'appmodulesitemap-wrapper.xml' ) } diff --git a/plugins/dataverse-xml-lsp/tests/fixtures/invalid/appmodulesitemap-wrapper.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/appmodulesitemap-wrapper.xml new file mode 100644 index 0000000..2865310 --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/appmodulesitemap-wrapper.xml @@ -0,0 +1,6 @@ + + test_app + + + + diff --git a/plugins/dataverse-xml-lsp/tests/fixtures/valid/appmodulesitemap-wrapper.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/appmodulesitemap-wrapper.xml new file mode 100644 index 0000000..1e1870a --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/fixtures/valid/appmodulesitemap-wrapper.xml @@ -0,0 +1,6 @@ + + test_app + + + + From 45becfc88ee4a89c48274ffe41e7efd1d99cc0ab Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Wed, 22 Jul 2026 12:40:41 +1200 Subject: [PATCH 6/7] Release dataverse-xml-lsp 2.2.0 and cloud-flow-json-lsp 2.1.0 --- CHANGELOG.md | 18 ++++++++++++++++++ .../.claude-plugin/plugin.json | 2 +- .../.claude-plugin/plugin.json | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e54b481..0858811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 2026-07-22 + +### dataverse-xml-lsp 2.2.0 + +- Validate pac's `` wrapper root (exported `SavedQueries/*.xml`) by extracting and + checking each inner `` against `Fetch.xsd` — indicative, like forms. Previously every + exported saved-query file failed as an unknown root. +- Validate the model-driven-app `` root by extracting its inner `` and + checking it against `SiteMap.xsd` — authoritative. +- Both new roots are covered by the PostToolUse hook. Removed the stale live + `**/SavedQueries/**/*.xml → Fetch.xsd` association, which mis-fired on the `` root. + +### cloud-flow-json-lsp 2.1.0 + +- The clientdata schema now validates actions nested inside `Scope`/`If`/`Foreach`/`Switch` + recursively, so a bogus `runAfter` status or a missing `type` on a nested action is caught instead + of passing silently. Top-level behaviour is unchanged. + ## 2.1.0 — 2026-07-21 ### dataverse-xml-lsp diff --git a/plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json b/plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json index b88014a..6d9b286 100644 --- a/plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json +++ b/plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/plugin.schema.json", "name": "cloud-flow-json-lsp", - "version": "2.0.0", + "version": "2.1.0", "description": "JSON LSP for unpacked Power Automate cloud-flow clientdata: live vscode-json-language-server diagnostics on solution Workflows/*.json against a bundled clientdata/WDL wrapper schema.", "author": { "name": "Connor Parsons" diff --git a/plugins/dataverse-xml-lsp/.claude-plugin/plugin.json b/plugins/dataverse-xml-lsp/.claude-plugin/plugin.json index 2dfde5e..8fb80e3 100644 --- a/plugins/dataverse-xml-lsp/.claude-plugin/plugin.json +++ b/plugins/dataverse-xml-lsp/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/plugin.schema.json", "name": "dataverse-xml-lsp", - "version": "2.1.0", + "version": "2.2.0", "description": "LSP + CLI validator for Dataverse customization XML: live lemminx diagnostics and a standalone PowerShell validator checking ribbon, sitemap, forms, FetchXML, charts and more against the official XSDs.", "author": { "name": "Connor Parsons" From ce35e4036b96950ad111a172e5a36384dc9cf0bf Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Wed, 22 Jul 2026 12:51:41 +1200 Subject: [PATCH 7/7] Refresh README for the LSP model, add use cases and roadmap; fix stale llms.txt links --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++----------- llms.txt | 10 +++++-- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5e2a7ed..1de6a9c 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,86 @@ # dataverse-agent-plugins -Agent plugins for Microsoft Dataverse / Power Platform development. A -[Claude Code plugin marketplace](https://docs.anthropic.com/en/docs/claude-code) — each -plugin also works standalone (plain PowerShell scripts) for other agents, VS Code, and CI. +Catch broken Dataverse / Power Platform customization files **while you edit them** — before +`pac solution import` fails at the end of a long round-trip. + +This is a [Claude Code plugin marketplace](https://docs.anthropic.com/en/docs/claude-code) with +two language-server (LSP) plugins. Each one validates a kind of hand-edited solution file against a +real schema and shows the errors inline as you type. Every plugin also runs **standalone** — plain +PowerShell/Node with no agent — so the same checks work from other agents, VS Code, or CI. + +## Plugins + +| Plugin | Validates | How | +|--------|-----------|-----| +| [dataverse-xml-lsp](plugins/dataverse-xml-lsp/) | Dataverse customization XML — ribbon (`RibbonDiff.xml`), sitemap, app sitemap, forms, saved queries, FetchXML, charts, ISV config, `Customizations.xml` | Live [lemminx](https://github.com/eclipse/lemminx) LSP diagnostics against the official Microsoft XSDs, plus a standalone PowerShell validator (`Validate-DataverseXml.ps1`) for wrapper files and CI | +| [cloud-flow-json-lsp](plugins/cloud-flow-json-lsp/) | Unpacked Power Automate cloud-flow clientdata (`Workflows/*.json`) | Live `vscode-json-language-server` diagnostics against a bundled clientdata/WDL wrapper schema; headless checks via built-in `Test-Json` | + +## When would I use this? + +- **You edit solution files as code.** You unpack a solution (`pac solution unpack`), tweak a ribbon + button, a form, a sitemap, or a cloud flow by hand, then pack and import. These plugins turn the + errors that would otherwise only appear at import time into red squiggles while you edit. +- **An agent is editing them for you.** In Claude Code (or Codex), the validator runs after edits so + the agent sees its own mistakes and fixes them, instead of confidently producing a file that won't + import. +- **You want a CI gate.** Run the standalone validator over changed files in a pipeline so a bad edit + fails the build, not the deployment. + +Neither plugin talks to Dataverse or needs auth — they validate files on disk. A successful +`pac solution import` / `pac solution check` remains the final authority; these catch the large +class of mistakes you don't need a live environment to find. ## Install (Claude Code) +Add the marketplace once: + /plugin marketplace add Cordedmink2/dataverse-agent-plugins + +Then install whichever plugin you need and run its one-time, per-machine setup (this fetches the +Microsoft XSDs / lemminx / JSON language server — nothing bulky is committed to the repo): + /plugin install dataverse-xml-lsp@dataverse-agent-plugins /dataverse-xml-lsp:dataverse-xml-lsp-setup -Update later with `/plugin marketplace update dataverse-agent-plugins` then -`/plugin update dataverse-xml-lsp@dataverse-agent-plugins` (re-run setup after updates). + /plugin install cloud-flow-json-lsp@dataverse-agent-plugins + /cloud-flow-json-lsp:cloud-flow-json-lsp-setup -## Plugins +Run `/reload-plugins` afterwards. Update later with +`/plugin marketplace update dataverse-agent-plugins`, then +`/plugin update @dataverse-agent-plugins`, and re-run that plugin's setup. + +## Use it without Claude Code -| Plugin | What it does | -|--------|--------------| -| [dataverse-xml-lsp](plugins/dataverse-xml-lsp/) | LSP + CLI validator for Dataverse customization XML (ribbon, sitemap, forms, FetchXML, charts, ISV config and more) against the official Microsoft XSDs. Live lemminx LSP diagnostics + a standalone PowerShell validator. Also usable from [Codex](plugins/dataverse-xml-lsp/docs/codex.md), [VS Code with no agent](plugins/dataverse-xml-lsp/docs/vscode.md), and CI. | -| [cloud-flow-json-lsp](plugins/cloud-flow-json-lsp/) | JSON LSP for unpacked Power Automate cloud-flow clientdata (`Workflows/*.json`), validated against a bundled clientdata/WDL wrapper schema. Live `vscode-json-language-server` LSP diagnostics; headless structure checks via built-in `Test-Json`. Also usable from [Codex](plugins/cloud-flow-json-lsp/docs/codex.md), [VS Code with no agent](plugins/cloud-flow-json-lsp/docs/vscode.md), and CI. | +Each plugin works from other tools — see its docs: + +- **Other agents (Codex, etc.):** `plugins//docs/codex.md` +- **VS Code, no agent:** `plugins//docs/vscode.md` +- **CI / headless:** the standalone validator (`Validate-DataverseXml.ps1`) or `Test-Json`; see the + same docs. ## Requirements -PowerShell 7+ (`pwsh`) on Windows, macOS or Linux. The `cloud-flow-json-lsp` plugin also -needs Node.js (for its `npm`-installed JSON language server). Plugins fetch what else they need -at setup (Microsoft XSDs, lemminx binary, the JSON language server) — nothing bulky or -third-party is committed here. +PowerShell 7+ (`pwsh`) on Windows, macOS, or Linux. `cloud-flow-json-lsp` also needs Node.js (for its +`npm`-installed JSON language server). Setup fetches the rest. + +## Roadmap + +Planned, not yet built (design detail in +[the validator-gaps spec](docs/superpowers/specs/2026-07-22-validator-gaps-design.md)): + +- **Authoritative saved-query validation.** Saved queries currently validate *indicatively* — the + bundled Microsoft `Fetch.xsd` lags real exports (`layoutxml`, `LocalizedNames`), so a few + expected "not declared" errors are noise, the same as whole-form `FormXml`. A setup-time patch + step would extend the schema so saved queries validate cleanly and the patch survives an XSD + re-download. +- **Semantic cloud-flow linting.** A layer above the JSON shape check that catches things a schema + can't express: `runAfter` naming a non-sibling action or a cycle, a `connectionName` that resolves + to no declared connection reference (the top cause of a flow importing turned Off), child-invoker + connection mistakes, hard-coded environment GUIDs / host / site URLs, and condition rows that are + empty or always-true (`{"equals":["",""]}`). Bundled into `cloud-flow-json-lsp` as its "semantics" + layer. ## License -MIT. The Microsoft XSD schemas are downloaded from Microsoft at setup time and remain -subject to Microsoft's terms — they are not redistributed in this repo. +MIT. The Microsoft XSD schemas are downloaded from Microsoft at setup time and remain subject to +Microsoft's terms — they are not redistributed in this repo. diff --git a/llms.txt b/llms.txt index e1ec6c4..330addd 100644 --- a/llms.txt +++ b/llms.txt @@ -13,7 +13,9 @@ - [Repo README](README.md): marketplace overview, install, requirements - [Plugin README](plugins/dataverse-xml-lsp/README.md): full setup for every consumer type -- [SKILL.md](plugins/dataverse-xml-lsp/SKILL.md): the agent skill — validation workflow, root-element→schema table, ribbon-button recipe, gotchas +- [validate skill](plugins/dataverse-xml-lsp/skills/dataverse-xml-validate/SKILL.md): when and how to run the validator +- [setup skill](plugins/dataverse-xml-lsp/skills/dataverse-xml-lsp-setup/SKILL.md): one-time per-machine install +- [guide.md](plugins/dataverse-xml-lsp/docs/guide.md): root-element→schema table (incl. savedqueries and AppModuleSiteMap wrapper roots), ribbon-button recipe, gotchas - [Codex guide](plugins/dataverse-xml-lsp/docs/codex.md): using the validator loop from non-Claude agents - [VS Code guide](plugins/dataverse-xml-lsp/docs/vscode.md): editor-only setup, CI usage - [Debugging](plugins/dataverse-xml-lsp/docs/debugging.md): setup failures, missing diagnostics, false positives @@ -22,7 +24,8 @@ ### cloud-flow-json-lsp - [Plugin README](plugins/cloud-flow-json-lsp/README.md): install + setup for every consumer type -- [SKILL.md](plugins/cloud-flow-json-lsp/SKILL.md): the shape-vs-semantics split, what attaches, gotchas +- [SKILL.md](plugins/cloud-flow-json-lsp/SKILL.md): one-time per-machine install +- [guide.md](plugins/cloud-flow-json-lsp/docs/guide.md): the shape-vs-semantics split, what attaches, recursive nested-action validation, gotchas - [Codex guide](plugins/cloud-flow-json-lsp/docs/codex.md): the `Test-Json` validation loop from non-Claude agents - [VS Code guide](plugins/cloud-flow-json-lsp/docs/vscode.md): editor-only setup, CI usage - [Debugging](plugins/cloud-flow-json-lsp/docs/debugging.md): setup failures, missing diagnostics @@ -30,5 +33,6 @@ ## Optional -- [Design spec](docs/superpowers/specs/2026-07-15-dataverse-agent-plugins-repo-design.md): why the repo is shaped this way +- [Repo design spec](docs/superpowers/specs/2026-07-15-dataverse-agent-plugins-repo-design.md): why the repo is shaped this way +- [Validator-gaps spec](docs/superpowers/specs/2026-07-22-validator-gaps-design.md): savedqueries + AppModuleSiteMap + nested-flow fixes and the roadmap behind them - [Changelog](CHANGELOG.md)