Skip to content

feat(shared): read items in pattern validation - #3060

Merged
markscott-ms merged 12 commits into
finos:mainfrom
YoofiTT96:feat/2859-validation-items-catalog
Sep 17, 2026
Merged

markscott-ms merged 12 commits into
finos:mainfrom
YoofiTT96:feat/2859-validation-items-catalog

Conversation

@YoofiTT96

@YoofiTT96 YoofiTT96 commented Sep 8, 2026

Copy link
Copy Markdown
Member

Description

A pattern can now declare nodes and relationships under items as well as prefixItems, so an architecture may add members rather than only choose between them. calm validate reads the new sites; generation, visualisation and the pattern differ follow separately.

Stacks on #3059, which exists because building this work is what exposed the prefixItems gaps. GitHub will not let a cross-fork PR target a branch in my fork, so the base is main and the diff carries #3059's commits. The commits to review are the last six.

Part of #2859.

The decision worth knowing

items members can coexist; prefixItems alternatives cannot. A prefixItems entry is one position, so one alternative wins. items covers every position after it, so any number of its members can be built.

That is what decides the uniqueness rules: two items nodes must not share a unique-id or an interface id, because both can appear. Two alternatives of one entry may. exclusiveGroup() is where this lives.

Gotcha

calm generate does not read items yet. An author can declare an items node, have calm validate accept the pattern, and get an architecture without that node, with nothing said. It closes when the generation PR lands, and PATTERN-DECISIONS.md records it meanwhile.

Rule changes

pattern-decision-must-be-declared-in-prefix-items is new. An items member may be left out, so a decision declared there can vanish and an answer never gets to decline it.

pattern-items-must-fit-within-max-items is new. maxItems counts the whole array and the prefixItems entries fill it from the front, so a pattern that leaves no room declares a member nothing can ever build.

pattern-prefix-items-must-declare-one-keyword becomes pattern-choice-must-declare-one-keyword, since it now covers items blocks too.

Ordering

Duplicate ids are blamed on the later declaration. Nothing ranked the two sites, so a pair spanning prefixItems and items was blamed backwards. Ordering is a property of the site — prefixItems fills the first positions and items fills everything after — and that rank now joins the parsed indices byBuildOrder compares. Every existing ordering test compared two declarations at the same kind of site, which is why none caught it; three that span both are added.

Tidying

decisionIsDeclaredInPrefixItems asked whether a relationship carries options with a JSONPath query over a path that has no wildcard. It now reads that path with lodash off a named constant, beside the one that locates a unique-id. Raised in review on #3059 against the same pattern elsewhere.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🎨 Code style/formatting changes
  • ♻️ Refactoring (no functional changes)
  • ⚡ Performance improvements
  • ✅ Test additions or updates
  • 🔧 Chore (maintenance, dependencies, CI, etc.)

Additive. No pattern in the repository changes result, because none of them uses items yet.

Affected Components

  • CLI (cli/)
  • Schema (calm/)
  • CALM AI (calm-ai/)
  • CALM Hub (calm-hub/)
  • CALM Hub UI (calm-hub-ui/)
  • CALM Server (calm-server/)
  • CALM Widgets (calm-widgets/)
  • Documentation (docs/)
  • Shared (shared/)
  • VS Code Extension (calm-plugins/vscode/)
  • Dependencies
  • CI/CD

cli/ is the JUnit fixture only. It carries two counts, a testsuites total and a Spectral Suite total, and both move.

Commit Message Format ✅

feat(shared): read items in pattern validation
fix(shared): order items declarations after prefixItems entries
feat(shared): reject maxItems that leaves no room for items
refactor(shared): read the decision marker without JSONPath

Testing

  • I have tested my changes locally
  • I have added/updated unit tests
  • All existing tests pass

161 spectral tests, 659 CLI tests including the end-to-end suite, shared at 107 files and 1238 tests, root npm test green. Every tracked pattern file was run against the real ruleset on main and on this branch; the findings are identical.

Rebased onto #3059 after its review round, so the ordering work here builds on the parsed-index comparator that landed there rather than on the string key it replaced.

Checklist

  • My commits follow the conventional commit format
  • I have updated documentation if necessary
  • I have added tests for my changes (if applicable)
  • My changes follow the project's coding standards

@github-actions github-actions Bot added cli Affects `cli` code shared labels Sep 8, 2026
@YoofiTT96
YoofiTT96 force-pushed the feat/2859-validation-items-catalog branch from 96d9ccd to e5aac0f Compare September 8, 2026 09:47
@YoofiTT96
YoofiTT96 force-pushed the feat/2859-validation-items-catalog branch 2 times, most recently from e858e3e to 8b6ea9d Compare September 11, 2026 11:10
@YoofiTT96 YoofiTT96 changed the title feat(shared): read items catalogs in pattern validation feat(shared): read items in pattern validation Sep 11, 2026
@YoofiTT96
YoofiTT96 force-pushed the feat/2859-validation-items-catalog branch from 8b6ea9d to 9624194 Compare September 11, 2026 11:18
A pattern may now declare nodes and relationships under items as well as
prefixItems, so an architecture can add members rather than only choose
between them. declaration-paths.ts gains the two items sites, and every
rule that resolves declarations picks them up without an edit of its
own, because none of them names a site.

items members are not mutually exclusive. Verified with Ajv: items
admits any number of matching elements, so two members can both be
built, while a prefixItems entry is one position and exactly one of its
alternatives is built. Two items members must therefore not share a
unique-id or an interface id, and the grouping helper is renamed
exclusiveGroup to say which declarations actually compete.

Adds pattern-decision-must-be-declared-in-prefix-items. An architecture
contains every relationship declared in prefixItems, so a decision there
is always asked. An items member may be left out, so a decision declared
in items can vanish and an answer never gets to decline it.

pattern-prefix-items-must-declare-one-keyword becomes
pattern-choice-must-declare-one-keyword, because it now covers items
blocks as well as prefixItems entries.

Additive: every tracked pattern in the repository produces the same
findings as before, because none of them uses items yet.
Duplicate ids were blamed on the earlier declaration whenever the pair
spanned prefixItems and items, because nothing ranked the two sites.
That broke the contract 960fa4f set, which is that the rule names the
later declaration.

Ordering is a property of the site: prefixItems fills the first
positions and items fills everything after. The rank joins the parsed
indices that byBuildOrder already compares.

Every ordering test compared two declarations at the same kind of site,
which is why none of them caught it. Adds three that span both.

Also records two limits the contract overstated. A decision declared as
one alternative of an entry can vanish just as one declared in items
can, and calm validate reports only the items case. A node declared
under items must sit inside oneOf or anyOf, because items applies one
schema to every position after the entries.

Renames entryPath to fixedPath so it pairs with fixedIdPath, and states
isAlternative against the regex rather than deriving it from two other
helpers.
maxItems counts the whole array and the prefixItems entries fill it from
the front, so an items member can only be built in what is left over. A
pattern that pins maxItems to the number of entries can never build one,
and calm generate produced an architecture that failed its own pattern
on maxItems.

The narrowed pattern cannot fix this. calm generate does not record a
decision's options in the architecture, so extractChoicesFromArchitecture
finds nothing and validation runs against the pattern as written.
An items block cannot limit how many times one member is used, so two
positions may both match it and the architecture holds one unique-id
twice. The pattern is correct, so calm validate cannot report it;
unique-ids-must-be-unique-in-architecture reports it later.

prefixItems is positional, so an architecture lists those elements first
and in order. Pre-existing, but items is the first construct that makes
an author think about array positions.

minItems: 0 is rejected, because pattern-has-no-empty-properties reads a
zero as a placeholder. Zero is the default, so the keyword can be
dropped. Issue finos#2859 writes the catalog with minItems: 0.
Both halves apply to the same element. If each pins a unique-id, no
element satisfies both and the position can never be filled. Verified
with Ajv, and calm generate still emits the selected alternative, so it
produces an architecture that fails its own pattern.

The note claimed the duplicate-id rule catches the common case. Sharing
an id is not the common case, and that rule reports a duplicate rather
than the loss.
The path has no wildcard, so a lodash read off a named constant does the
same work and keeps the marker's location beside the id's.
@YoofiTT96
YoofiTT96 requested review from a team and markscott-ms as code owners September 17, 2026 10:07

@markscott-ms markscott-ms left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General problems in this PR:

shared/src/spectral/functions/pattern/is-defined-in-oneof-or-anyof.spec.ts
● 1 [test-coverage] No test exercises a decision option legitimately referencing a node/relationship declared under items

General notes (some for later PR, recognising that this one only targets spectral validation rules)

calm-hub-ui/src/visualizer/components/reactflow/utils/patternTransformer.ts
● 32 [correctness] Pattern visualizer's getPrefixItems only reads prefixItems, never items, so items-declared nodes/relationships vanish from the rendered graph

calm-models/src/diff/pattern-diff.ts
● 50 [correctness] diffPatterns only inspects prefixItems, never items, when normalising patterns for comparison

calm-plugins/vscode/src/webview/panels/PatternPicker.tsx
● 66 [correctness] VSCode PatternPicker's instantiateFromPattern/extractValue only recurse into prefixItems, never items

Comment thread shared/src/spectral/rules-pattern.ts
Comment thread shared/src/spectral/rules-pattern.ts
Comment thread shared/src/spectral/rules-pattern.ts
Comment thread shared/src/spectral/rules-pattern.ts
Comment thread shared/src/spectral/functions/pattern/items-fit-within-max-items.ts Outdated
YoofiTT96 and others added 3 commits September 17, 2026 11:17
A decision holds its answers in an options array, and calm generate reads
options prefixItems alone. Nothing stopped an author declaring the option
blocks in options items instead, where validation passed, generation
ignored them, and the decision lost its answers with no message.

The four rules that read decision options all select options prefixItems,
so widening each to reach options items would validate a shape the
generator cannot build. The new rule rejects the shape instead.

itemsFitWithinMaxItems treated an empty oneOf or anyOf as a choice,
because an empty array is truthy. It reported that maxItems left no room
for a member the block never declared.

Raised in review on finos#3060.
Nothing asserted that a decision option can name a node or relationship
declared in an items catalogue, so the guarantee was free to regress.

The permissive half alone would pass trivially, because the rule reports
only when the referenced id is a fixed prefixItems entry. The fixed-entry
case is pinned beside it so both sides of the condition are covered.

Raised in review on finos#3060.
The fixture pins the rule inventory, so adding a rule changes both totals
and the rule list.
Comment thread cli/test_fixtures/validate_output_junit.xml
@markscott-ms
markscott-ms merged commit 35266bf into finos:main Sep 17, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Affects `cli` code shared

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants