Skip to content

feat: implement metadata filtering in view predicates (Implements #2754)#2769

Merged
davydkov merged 4 commits intolikec4:mainfrom
galuszkak:feature/metadata-views-filtering
Mar 19, 2026
Merged

feat: implement metadata filtering in view predicates (Implements #2754)#2769
davydkov merged 4 commits intolikec4:mainfrom
galuszkak:feature/metadata-views-filtering

Conversation

@galuszkak
Copy link
Copy Markdown
Contributor

@galuszkak galuszkak commented Mar 17, 2026

This PR implements View predicates on metadata mentioned in #2754. I did help myself a lot with Claude given I'm unfamiliar with this codebase.

Checklist

  • I've thoroughly read the latest contribution guidelines.
  • I've rebased my branch onto main before creating this PR.
  • My commit messages follow conventional spec
  • I've added tests to cover my changes (if applicable).
  • I've verified that all new and existing tests have passed locally for mobile, tablet, and desktop screen sizes.
    I had to regenerate screenshots in E2E tests.
  • My change requires documentation updates.
  • I've updated the documentation accordingly.

Summary by CodeRabbit

  • New Features

    • Add metadata-based filtering for views and deployment views (by key, existence, value, negation, and participant-scoped source/target).
    • Metadata is now exposed on models and computed view nodes so predicates can evaluate and results include metadata.
    • Added sample metadata-based example views and config.
  • Editor

    • Syntax and semantic highlighting for metadata.property and metadata predicates.
  • Documentation

    • Added metadata filtering guide with examples.
  • Tests

    • Extensive tests covering metadata operators and view filtering.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 17, 2026

🦋 Changeset detected

Latest commit: 554bf8e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@likec4/core Minor
@likec4/language-server Minor
@likec4/playground Minor
@likec4/config Minor
@likec4/diagram Minor
@likec4/generators Minor
@likec4/language-services Minor
@likec4/layouts Minor
@likec4/leanix-bridge Minor
likec4 Minor
@likec4/mcp Minor
@likec4/react Minor
@likec4/vite-plugin Minor
@likec4/vscode-preview Minor
likec4-vscode Minor
@likec4/docs-astro Minor
@likec4/style-preset Minor
@likec4/styles Minor
@likec4/log Minor
@likec4/tsconfig Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds metadata-aware filtering and propagation across grammar, parser, language server (grammar/semantic tokens/formatter), generators, core models, computed-view propagation and predicates, tests, examples, and syntax highlighting for metadata.<key> access.

Changes

Cohort / File(s) Summary
Syntax Highlighting
apps/docs/likec4.tmLanguage.json, apps/playground/likec4.tmLanguage.json, packages/vscode/likec4.tmLanguage.json
Add TextMate rules to recognize metadata.<identifier> with optional is / is not captures for highlighting.
Docs & Examples
apps/docs/src/content/docs/dsl/Views/predicates.mdx, examples/metadata-views/likec4.config.json, examples/metadata-views/views.c4
Add documentation subsection and an example project demonstrating metadata-based where filters and example views.
Grammar
packages/language-server/src/like-c4.langium
Introduce metadata where-clauses for elements, relations, and participants; allow boolean metadata values; add MetadataFilterValue rule.
Parser
packages/language-server/src/model/model-parser-where.ts, packages/language-server/src/model/parser/Base.ts
Parse metadata equality/inequality and boolean metadata; add parseMetadataEquals and extend participant/where parsing and metadata extraction.
LSP: Tokens & Formatter
packages/language-server/src/lsp/SemanticTokenProvider.ts, packages/language-server/src/formatting/LikeC4Formatter.ts
Adjust semantic token grouping to handle metadata where variants; tighten where-expression formatting conditions to element/relation node checks.
Generators & Schemas
packages/generators/src/likec4/operators/expressions.ts, packages/generators/src/likec4/operators/expressions.spec.ts, packages/generators/src/likec4/schemas/expression.ts
Add whereMetadataEqual generator and helper quoteMetadataValue; extend schemas and tests to emit/validate metadata operator syntax (including boolean handling and participant-scoped metadata).
Core Types & Operators
packages/core/src/types/operators.ts, packages/core/src/types/operators.spec.ts, packages/core/src/types/view-computed.ts
Add MetadataEqual operator type and guard; extend WhereOperator/ParticipantOperator; add metadata to Filterable and ComputedNode; add metadata operator tests.
Model Accessors
packages/core/src/model/ElementModel.ts, packages/core/src/model/RelationModel.ts, packages/core/src/model/DeploymentElementModel.ts
Expose public metadata getters on ElementModel, RelationshipModel, DeploymentNodeModel, DeployedInstanceModel, and DeploymentRelationModel.
Computed Views & Predicates
packages/core/src/compute-view/utils/buildComputedNodes.ts, packages/core/src/compute-view/utils/relationExpressionToPredicates.ts, packages/core/src/compute-view/deployment-view/predicates/utils.ts
Propagate metadata into ComputedNode sources, filterable edges/relations, and predicate inputs so metadata is available during predicate evaluation.
Tests
packages/language-server/src/__tests__/deployment-views.spec.ts, packages/language-server/src/__tests__/views-2.spec.ts, packages/likec4/src/LikeC4.spec.ts, packages/generators/src/likec4/operators/expressions.spec.ts, packages/core/src/types/operators.spec.ts
Add/extend tests validating metadata-based view filters, generator output, and operator behavior (including arrays and boolean string handling).
Changeset
.changeset/metadata-views-filtering.md
Add changeset documenting metadata filtering support and bump package versions.

Sequence Diagram(s)

sequenceDiagram
    participant User as DSL / Editor
    participant Parser as Langium Parser
    participant LSP as Language Server
    participant Generator as LikeC4 Generator
    participant Model as Core Model
    participant ViewComp as View Computation
    participant Predicate as Predicate Engine
    participant Output as Computed Views

    User->>Parser: Parse "where metadata.env is \"prod\""
    Parser->>LSP: Emit AST (WhereMetadataEqual)
    LSP->>Generator: Request formatting / schema handling
    Generator->>ViewComp: Produce predicate representation
    ViewComp->>Model: Read element/relation metadata via getters
    Model-->>ViewComp: Return metadata record
    ViewComp->>Predicate: Evaluate metadata predicate against record
    Predicate-->>ViewComp: Match / No-match
    ViewComp->>Output: Include or exclude node
    Output->>User: Render filtered view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • davydkov

Poem

🐰 I sniffed the metadata, keys tucked in tight,
Hopped through views by day and filtered by night.
Prod, staging, version — I sort with a twirl,
Tiny keys, little values, each like a pearl.
Hooray — views bloom bright where data takes flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing metadata filtering in view predicates, directly addressing the feature work and referencing the related issue.
Description check ✅ Passed The description follows the template structure and includes all required checklist items marked complete. The author provided context about using Claude for assistance and confirmed comprehensive testing and documentation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/compute-view/deployment-view/predicates/utils.ts (1)

250-258: ⚠️ Potential issue | 🟠 Major

Use the deployed instance’s metadata in this enriched filterable shape.

This branch already enriches tags from connectionEndpoint, but metadata still comes from the logical element. source/target metadata predicates on model relations in deployment views will ignore instance overrides and can match the wrong connections.

Suggested fix
     return {
       kind: relationEndpoint.kind,
       tags: [...relationEndpoint.tags, ...(deployedInstance?.tags ?? [])],
-      metadata: relationEndpoint.metadata,
+      metadata: deployedInstance ? deployedInstance.metadata : relationEndpoint.metadata,
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/compute-view/deployment-view/predicates/utils.ts` around
lines 250 - 258, The returned enriched shape for element relations currently
uses relationEndpoint.metadata instead of instance overrides; change the
metadata field to use the deployed instance's metadata when present by replacing
relationEndpoint.metadata with deployedInstance?.metadata ??
relationEndpoint.metadata in the branch that checks
isElementModel<M>(relationEndpoint) and computes deployedInstance (see
isDeploymentElementModel<M>, isDeployedInstance, relationEndpoint,
connectionEndpoint, deployedInstance, and the returned object with
kind/tags/metadata).
🧹 Nitpick comments (4)
packages/language-server/src/model/parser/Base.ts (1)

248-255: Consider guarding against undefined boolValue.

If neither p.value nor p.boolValue is set (e.g., during parser error recovery or AST edge cases), String(p.boolValue) evaluates to "undefined", which is truthy and would pass the filter on line 255, resulting in metadata entries with the literal string "undefined".

While the grammar suggests one must always be present, a defensive check would make this more robust.

🛡️ Proposed defensive fix
       flatMap(p => {
         if (p.value) {
           return extractValues(p.value).map(v => [p.key, v] as [string, string])
         }
         // Boolean metadata values (e.g. critical true)
+        if (p.boolValue !== undefined) {
+          return [[p.key, String(p.boolValue)] as [string, string]]
+        }
-        return [[p.key, String(p.boolValue)] as [string, string]]
+        return []
       }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/model/parser/Base.ts` around lines 248 - 255,
The flatMap/filter chain can emit ["key","undefined"] when neither p.value nor
p.boolValue is present; modify the flatMap handling (around p.value /
p.boolValue usage) to guard that p.boolValue is not undefined before returning
String(p.boolValue) — only return the boolean branch if typeof p.boolValue !==
"undefined" (or equivalent explicit check); otherwise return an empty array so
the filter and downstream code never see the literal "undefined". Ensure this
change is applied where extractValues, p.value and p.boolValue are referenced in
the same mapping block.
packages/core/src/types/view-computed.ts (1)

39-45: Extract metadata shape into a shared core type alias.

Line 44 introduces an inline metadata record shape that is repeated in other changed TS files. Pull this into a shared exported type (e.g., ComputedMetadata) to avoid drift.

♻️ Suggested refactor
+export type ComputedMetadata = Readonly<Record<string, string | string[] | undefined>> | null
+
 export interface ComputedNode<A extends AnyAux = AnyAux>
   extends
     aux.WithTags<A>,
     aux.WithOptionalLinks
 {
@@
-  readonly metadata?: Readonly<Record<string, string | string[] | undefined>> | null
+  readonly metadata?: ComputedMetadata

As per coding guidelines, **/*.{ts,tsx}: Agent types and interfaces should be defined separately and reused across the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/types/view-computed.ts` around lines 39 - 45, Extract the
inline metadata record used on the metadata property into a shared exported type
alias named ComputedMetadata; declare export type ComputedMetadata =
Readonly<Record<string, string | string[] | undefined>> | null and replace the
inline annotation on the metadata property (and any other duplicated occurrences
across changed TS files) with ComputedMetadata, updating imports/exports so all
modules reference the single alias instead of repeating the Record shape.
packages/language-server/src/model/model-parser-where.ts (1)

5-16: Prefer switch(true) here to match repository control-flow style.

The new helper works, but this block currently uses an if-chain.

🧩 Style-aligned alternative
 const parseMetadataEquals = (
   { operator, not }: ast.WhereMetadataEqual,
   value: string,
 ): c4.EqualOperator<string> => {
-  if (operator?.startsWith('!=')) {
-    return { neq: value }
-  }
-  if (operator?.startsWith('=')) {
-    return { eq: value }
-  }
-  return not ? { neq: value } : { eq: value }
+  switch (true) {
+    case operator?.startsWith('!='):
+      return { neq: value }
+    case operator?.startsWith('='):
+      return { eq: value }
+    default:
+      return not ? { neq: value } : { eq: value }
+  }
 }

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Favor switch(true) over if-else chains.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/model/model-parser-where.ts` around lines 5 -
16, The parseMetadataEquals function uses an if-chain; refactor it to use a
switch(true) style to match repository control-flow guidelines: inside
parseMetadataEquals (accepting ast.WhereMetadataEqual and returning
c4.EqualOperator<string>) replace the conditional checks for
operator?.startsWith('!=') and operator?.startsWith('=') and the final not ?
branch with a switch(true) that returns { neq: value } for the '!=’ case, { eq:
value } for the '=' case, and the not ? { neq: value } : { eq: value } default;
keep the same semantics and return types.
packages/core/src/model/ElementModel.ts (1)

98-100: Use a cast-free metadata getter here as well.

Line 99 has the same unsafe cast pattern; mirror the return this.getMetadata() approach to keep this type-safe.

As per coding guidelines, **/*.{ts,tsx}: TypeScript-first repo; use explicit types. Avoid using any, casts with as.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/model/ElementModel.ts` around lines 98 - 100, The metadata
getter in ElementModel currently returns (this.$element.metadata ?? {}) as
aux.Metadata<A>, which uses an unsafe cast; change the getter to return
this.getMetadata() (i.e., delegate to the existing getMetadata() method) so the
result is produced with proper typing instead of an explicit cast—update the
metadata getter in class ElementModel to call getMetadata() and remove the
casted return.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/model/DeploymentElementModel.ts`:
- Around line 443-449: The metadata getter now falls back to element.metadata
but AbstractDeploymentElementModel.hasMetadata() and getMetadata() still only
check this.$instance.metadata, causing inconsistent behavior; update
hasMetadata() and getMetadata(key) to consult the same fallback logic as the
metadata getter (i.e., check this.$instance.metadata first, then
this.element.metadata, then {}), using the same types as metadata() so that
hasMetadata() returns true when either instance or element metadata exist and
getMetadata(key) returns the value from instance metadata or, if missing, from
element.metadata.

In `@packages/core/src/model/RelationModel.ts`:
- Around line 126-128: The metadata getter in RelationModel currently returns
(this.$relationship.metadata ?? {}) as aux.Metadata<A>; change it to call and
return this.getMetadata() instead so you use the existing typed accessor
(getMetadata) rather than an unsafe cast; update the getter in the RelationModel
class to return this.getMetadata() and remove the casted expression.

In `@packages/core/src/types/operators.ts`:
- Around line 135-140: The metadata-existence check in the isMetadataEqual
branch uses the `key in item.metadata` operator which matches inherited
properties; update the predicate returned by isMetadataEqual (the block handling
operator.metadata when value === undefined) to use
Object.prototype.hasOwnProperty.call(item.metadata, key) and also ensure
item.metadata is non-null before calling it (preserve the existing
`item.metadata != null` check) so only own properties are considered when
testing existence.

In `@packages/language-server/src/lsp/SemanticTokenProvider.ts`:
- Around line 163-167: The current visitor block registers
ast.isWhereRelationKind in the isAnyOf check and then marks
mark.property('value').readonly.definition.type(), which duplicates the earlier
handling of WhereRelationKind and causes double semantic tokens; remove
ast.isWhereRelationKind from the isAnyOf list (or otherwise add a guard to skip
when the node was already handled) so only one visitor emits the token for
WhereRelationKind.value, keeping the
mark.property('value').readonly.definition.type() behavior for the remaining
kinds.

In `@packages/vscode/likec4.tmLanguage.json`:
- Around line 21-28: The regex in the "match" field currently uses
(is(\\s+not)?) which can match "is" as part of longer words (causing false
positives like "island"); update that capture to enforce word boundaries around
the "is" operator (for example replace the group with a version that uses
\\bis\\b and applies the optional "\\s+not" token with its own word boundary),
keeping the outer structure and the same capture index for the "is" token so the
captures 1/2/3 remain unchanged.

---

Outside diff comments:
In `@packages/core/src/compute-view/deployment-view/predicates/utils.ts`:
- Around line 250-258: The returned enriched shape for element relations
currently uses relationEndpoint.metadata instead of instance overrides; change
the metadata field to use the deployed instance's metadata when present by
replacing relationEndpoint.metadata with deployedInstance?.metadata ??
relationEndpoint.metadata in the branch that checks
isElementModel<M>(relationEndpoint) and computes deployedInstance (see
isDeploymentElementModel<M>, isDeployedInstance, relationEndpoint,
connectionEndpoint, deployedInstance, and the returned object with
kind/tags/metadata).

---

Nitpick comments:
In `@packages/core/src/model/ElementModel.ts`:
- Around line 98-100: The metadata getter in ElementModel currently returns
(this.$element.metadata ?? {}) as aux.Metadata<A>, which uses an unsafe cast;
change the getter to return this.getMetadata() (i.e., delegate to the existing
getMetadata() method) so the result is produced with proper typing instead of an
explicit cast—update the metadata getter in class ElementModel to call
getMetadata() and remove the casted return.

In `@packages/core/src/types/view-computed.ts`:
- Around line 39-45: Extract the inline metadata record used on the metadata
property into a shared exported type alias named ComputedMetadata; declare
export type ComputedMetadata = Readonly<Record<string, string | string[] |
undefined>> | null and replace the inline annotation on the metadata property
(and any other duplicated occurrences across changed TS files) with
ComputedMetadata, updating imports/exports so all modules reference the single
alias instead of repeating the Record shape.

In `@packages/language-server/src/model/model-parser-where.ts`:
- Around line 5-16: The parseMetadataEquals function uses an if-chain; refactor
it to use a switch(true) style to match repository control-flow guidelines:
inside parseMetadataEquals (accepting ast.WhereMetadataEqual and returning
c4.EqualOperator<string>) replace the conditional checks for
operator?.startsWith('!=') and operator?.startsWith('=') and the final not ?
branch with a switch(true) that returns { neq: value } for the '!=’ case, { eq:
value } for the '=' case, and the not ? { neq: value } : { eq: value } default;
keep the same semantics and return types.

In `@packages/language-server/src/model/parser/Base.ts`:
- Around line 248-255: The flatMap/filter chain can emit ["key","undefined"]
when neither p.value nor p.boolValue is present; modify the flatMap handling
(around p.value / p.boolValue usage) to guard that p.boolValue is not undefined
before returning String(p.boolValue) — only return the boolean branch if typeof
p.boolValue !== "undefined" (or equivalent explicit check); otherwise return an
empty array so the filter and downstream code never see the literal "undefined".
Ensure this change is applied where extractValues, p.value and p.boolValue are
referenced in the same mapping block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b71a8b51-c02a-480d-83bb-3db495d8c092

📥 Commits

Reviewing files that changed from the base of the PR and between 585d4bb and 1224211.

⛔ Files ignored due to path filters (50)
  • e2e/tests/__screenshots__/chromium-darwin/e2e-amazon-lambdas.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-amazon-rds.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-amazon-s3.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-amazon-sqs.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-amazon.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-backend.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-legacy-backend.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-legacy.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-next.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-to-amazon-sequence.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-to-amazon.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-ui-dashboard.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-ui-supportPanel.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud-ui.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-cloud.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-customer.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-deploy-1.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-dynamic-view-1-sequence.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-dynamic-view-1.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-graphql.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-index.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-mobile.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/e2e-view-with-custom-colors.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-darwin/issue-2282-index.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-amazon-lambdas.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-amazon-rds.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-amazon-s3.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-amazon-sqs.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-amazon.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-backend.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-legacy-backend.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-legacy.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-next.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-to-amazon-sequence.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-to-amazon.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-ui-dashboard.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-ui-supportPanel.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud-ui.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-cloud.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-customer.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-deploy-1.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-dynamic-view-1-sequence.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-dynamic-view-1.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-graphql.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-index.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-mobile.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/e2e-view-with-custom-colors.png is excluded by !**/*.png
  • e2e/tests/__screenshots__/chromium-linux/issue-2282-index.png is excluded by !**/*.png
  • packages/generators/src/model/__snapshots__/likec4.computed-model-with-core.snap is excluded by !**/*.snap
  • packages/generators/src/model/__snapshots__/likec4.computed-model.snap is excluded by !**/*.snap
📒 Files selected for processing (26)
  • apps/docs/likec4.tmLanguage.json
  • apps/docs/src/content/docs/dsl/Views/predicates.mdx
  • apps/playground/likec4.tmLanguage.json
  • examples/metadata-views/likec4.config.json
  • examples/metadata-views/views.c4
  • packages/core/src/compute-view/deployment-view/predicates/utils.ts
  • packages/core/src/compute-view/utils/buildComputedNodes.ts
  • packages/core/src/compute-view/utils/relationExpressionToPredicates.ts
  • packages/core/src/model/DeploymentElementModel.ts
  • packages/core/src/model/ElementModel.ts
  • packages/core/src/model/RelationModel.ts
  • packages/core/src/types/operators.spec.ts
  • packages/core/src/types/operators.ts
  • packages/core/src/types/view-computed.ts
  • packages/generators/src/likec4/operators/expressions.spec.ts
  • packages/generators/src/likec4/operators/expressions.ts
  • packages/generators/src/likec4/schemas/expression.ts
  • packages/language-server/src/__tests__/deployment-views.spec.ts
  • packages/language-server/src/__tests__/views-2.spec.ts
  • packages/language-server/src/formatting/LikeC4Formatter.ts
  • packages/language-server/src/like-c4.langium
  • packages/language-server/src/lsp/SemanticTokenProvider.ts
  • packages/language-server/src/model/model-parser-where.ts
  • packages/language-server/src/model/parser/Base.ts
  • packages/likec4/src/LikeC4.spec.ts
  • packages/vscode/likec4.tmLanguage.json

Comment on lines +443 to +449
/**
* Instance metadata overrides element metadata entirely (replacement, not merge).
* This differs from tags which are merged from instance + element.
*/
get metadata(): aux.Metadata<A> {
return (this.$instance.metadata ?? this.element.metadata ?? {}) as aux.Metadata<A>
}
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.

⚠️ Potential issue | 🟠 Major

Keep the metadata helpers consistent with the new instance fallback.

metadata now falls back to element.metadata, but AbstractDeploymentElementModel.hasMetadata() / getMetadata() still read only this.$instance.metadata. An instance without local metadata will expose metadata while reporting hasMetadata() === false and getMetadata('...') === undefined.

Suggested fix
 abstract class AbstractDeploymentElementModel<A extends Any> implements WithTags<A>, WithMetadata<A> {
+  abstract get metadata(): aux.Metadata<A>
+
   public hasMetadata(): boolean {
-    return !!this.$node.metadata && !isEmpty(this.$node.metadata)
+    return !isEmpty(this.metadata)
   }

   public getMetadata(): aux.Metadata<A>
   public getMetadata(field: aux.MetadataKey<A>): string | string[] | undefined
   public getMetadata(field?: aux.MetadataKey<A>) {
     if (field) {
-      return this.$node.metadata?.[field]
+      return this.metadata[field]
     }
-    return this.$node.metadata ?? {}
+    return this.metadata
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/model/DeploymentElementModel.ts` around lines 443 - 449,
The metadata getter now falls back to element.metadata but
AbstractDeploymentElementModel.hasMetadata() and getMetadata() still only check
this.$instance.metadata, causing inconsistent behavior; update hasMetadata() and
getMetadata(key) to consult the same fallback logic as the metadata getter
(i.e., check this.$instance.metadata first, then this.element.metadata, then
{}), using the same types as metadata() so that hasMetadata() returns true when
either instance or element metadata exist and getMetadata(key) returns the value
from instance metadata or, if missing, from element.metadata.

Comment on lines +126 to +128
get metadata(): aux.Metadata<A> {
return (this.$relationship.metadata ?? {}) as aux.Metadata<A>
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "as aux\\.Metadata<" packages/core/src/model/RelationModel.ts packages/core/src/model/ElementModel.ts

Repository: likec4/likec4

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Get the context around the metadata getter and examine the class structure
sed -n '100,150p' packages/core/src/model/RelationModel.ts | cat -n

Repository: likec4/likec4

Length of output: 1814


🏁 Script executed:

#!/bin/bash
# Check if getMetadata() method exists in RelationModel.ts
rg -n "getMetadata" packages/core/src/model/RelationModel.ts -A 2

Repository: likec4/likec4

Length of output: 313


🏁 Script executed:

#!/bin/bash
# Compare with ElementModel.ts to see the pattern
rg -n "metadata" packages/core/src/model/ElementModel.ts | head -20

Repository: likec4/likec4

Length of output: 328


🏁 Script executed:

#!/bin/bash
# Get the full getMetadata() implementation in RelationModel.ts
sed -n '174,185p' packages/core/src/model/RelationModel.ts | cat -n

Repository: likec4/likec4

Length of output: 496


Replace the as aux.Metadata<A> cast with the existing getMetadata() method.

At line 127, use this.getMetadata() instead of casting. The method already returns the correct type without requiring an unsafe cast.

  get metadata(): aux.Metadata<A> {
-   return (this.$relationship.metadata ?? {}) as aux.Metadata<A>
+   return this.getMetadata()
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get metadata(): aux.Metadata<A> {
return (this.$relationship.metadata ?? {}) as aux.Metadata<A>
}
get metadata(): aux.Metadata<A> {
return this.getMetadata()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/model/RelationModel.ts` around lines 126 - 128, The
metadata getter in RelationModel currently returns (this.$relationship.metadata
?? {}) as aux.Metadata<A>; change it to call and return this.getMetadata()
instead so you use the existing typed accessor (getMetadata) rather than an
unsafe cast; update the getter in the RelationModel class to return
this.getMetadata() and remove the casted expression.

Comment on lines +135 to +140
case isMetadataEqual(operator): {
const { key, value } = operator.metadata
if (value === undefined) {
return (item) => {
return item.metadata != null && key in item.metadata && item.metadata[key] != null
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the exact code at lines 135-140
cat -n packages/core/src/types/operators.ts | sed -n '130,150p'

Repository: likec4/likec4

Length of output: 942


🏁 Script executed:

# Check the full metadata operator implementation context
cat -n packages/core/src/types/operators.ts | sed -n '120,165p'

Repository: likec4/likec4

Length of output: 1998


🏁 Script executed:

# Check the type definitions for metadata
rg -A 5 -B 5 "metadata.*:" packages/core/src/types/operators.ts | head -60

Repository: likec4/likec4

Length of output: 1006


🏁 Script executed:

# Check what the metadata structure is expected to be
rg "item\.metadata" packages/core/src/types/operators.ts -A 2 -B 2

Repository: likec4/likec4

Length of output: 764


🏁 Script executed:

# Check if there are other places in the codebase using similar patterns
rg "in .*metadata" --type ts --type tsx

Repository: likec4/likec4

Length of output: 84


🏁 Script executed:

# Let me search for other patterns with metadata checks using correct flags
rg "in.*metadata" packages/core/src/types/ -A 2 -B 2

Repository: likec4/likec4

Length of output: 9352


🏁 Script executed:

# Check how metadata objects are created/populated in the codebase
rg "metadata\s*=" packages/core/src/ -B 2 -A 2 | head -80

Repository: likec4/likec4

Length of output: 39


🏁 Script executed:

# Look for any test cases that might show expected behavior
find packages/core -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "metadata" 2>/dev/null | head -5

Repository: likec4/likec4

Length of output: 156


🏁 Script executed:

# Check if metadata is ever accessed with prototype-chain properties
rg "metadata\[.*constructor" packages/core/src/

Repository: likec4/likec4

Length of output: 39


🏁 Script executed:

# Verify the exact type definition of metadata to understand if prototype pollution is expected
cat -n packages/core/src/types/operators.ts | sed -n '85,100p'

Repository: likec4/likec4

Length of output: 612


🏁 Script executed:

# Check if there are other property-in checks in the codebase that might have same issue
rg " in " packages/core/src/types/operators.ts

Repository: likec4/likec4

Length of output: 519


🏁 Script executed:

# Look at the test for metadata existence check to understand expected behavior
cat -n packages/core/src/types/operators.spec.ts | grep -A 15 "metadata existence check"

Repository: likec4/likec4

Length of output: 1519


🏁 Script executed:

# Check if metadata objects are always plain objects or could have custom prototypes
rg "Object.create\|Object.setPrototypeOf" packages/core/src/

Repository: likec4/likec4

Length of output: 39


🏁 Script executed:

# Search for any documented behavior or comments about metadata handling
rg -B 5 "metadata.*key" packages/core/src/types/ | head -50

Repository: likec4/likec4

Length of output: 3825


Use hasOwnProperty() for own-key-only metadata checks.

Line 139 uses key in item.metadata, which walks the prototype chain. Filters checking for inherited property names like constructor or toString would incorrectly match. Use Object.prototype.hasOwnProperty.call() instead.

Suggested fix
       if (value === undefined) {
         return (item) => {
-          return item.metadata != null && key in item.metadata && item.metadata[key] != null
+          return item.metadata != null
+            && Object.prototype.hasOwnProperty.call(item.metadata, key)
+            && item.metadata[key] != null
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case isMetadataEqual(operator): {
const { key, value } = operator.metadata
if (value === undefined) {
return (item) => {
return item.metadata != null && key in item.metadata && item.metadata[key] != null
}
case isMetadataEqual(operator): {
const { key, value } = operator.metadata
if (value === undefined) {
return (item) => {
return item.metadata != null
&& Object.prototype.hasOwnProperty.call(item.metadata, key)
&& item.metadata[key] != null
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/types/operators.ts` around lines 135 - 140, The
metadata-existence check in the isMetadataEqual branch uses the `key in
item.metadata` operator which matches inherited properties; update the predicate
returned by isMetadataEqual (the block handling operator.metadata when value ===
undefined) to use Object.prototype.hasOwnProperty.call(item.metadata, key) and
also ensure item.metadata is non-null before calling it (preserve the existing
`item.metadata != null` check) so only own properties are considered when
testing existence.

Comment on lines +21 to +28
{
"match": "\\b(metadata)\\.([a-zA-Z_]\\w*)(?:\\s+(is(\\s+not)?))?",
"captures": {
"1": { "name": "keyword.control.likec4" },
"2": { "name": "variable.other.property.likec4" },
"3": { "name": "keyword.control.likec4" }
}
},
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import re
p = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(\s+not)?))?')
tests = [
  'metadata.env',
  'metadata.env is prod',
  'metadata.env is not prod',
  'metadata.env island'
]
for t in tests:
    m = p.search(t)
    print(f'{t!r} -> {m.group(0)!r}' if m else f'{t!r} -> None')
PY

Repository: likec4/likec4

Length of output: 228


🏁 Script executed:

# Check if the file exists and view lines around 21-28
fd likec4.tmLanguage.json -x wc -l {}

Repository: likec4/likec4

Length of output: 185


🏁 Script executed:

# Read lines 21-28 from the file
fd likec4.tmLanguage.json -x sed -n '20,30p' {}

Repository: likec4/likec4

Length of output: 965


🏁 Script executed:

# Test the suggested regex fix
python - <<'PY'
import re

# Original regex (current)
original = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(\s+not)?))?')

# Suggested fix (with trailing \b and non-capturing group)
suggested = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(?:\s+not)?))?\\b')

tests = [
  'metadata.env',
  'metadata.env is prod',
  'metadata.env is not prod',
  'metadata.env island',
  'metadata.description is important',
]

print("ORIGINAL REGEX:")
for t in tests:
    m = original.search(t)
    print(f'  {t!r:40} -> {m.group(0)!r if m else "None"}')

print("\nSUGGESTED REGEX (with trailing \\b):")
for t in tests:
    m = suggested.search(t)
    print(f'  {t!r:40} -> {m.group(0)!r if m else "None"}')
PY

Repository: likec4/likec4

Length of output: 239


🏁 Script executed:

python - <<'PY'
import re

# Original regex (current)
original = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(\s+not)?))?')

# Suggested fix (with trailing \b and non-capturing group)
suggested = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(?:\s+not)?))?\\b')

tests = [
  'metadata.env',
  'metadata.env is prod',
  'metadata.env is not prod',
  'metadata.env island',
  'metadata.description is important',
]

print("ORIGINAL REGEX:")
for t in tests:
    m = original.search(t)
    result = m.group(0) if m else "None"
    print("  " + repr(t).ljust(40) + " -> " + repr(result))

print("\nSUGGESTED REGEX (with trailing \\b):")
for t in tests:
    m = suggested.search(t)
    result = m.group(0) if m else "None"
    print("  " + repr(t).ljust(40) + " -> " + repr(result))
PY

Repository: likec4/likec4

Length of output: 703


🏁 Script executed:

# Verify JSON formatting with dprint
fd likec4.tmLanguage.json -path "*/packages/vscode/*" -x cat {}

Repository: likec4/likec4

Length of output: 225


🏁 Script executed:

# Read the actual file content around the metadata regex
sed -n '15,35p' ./packages/vscode/likec4.tmLanguage.json

Repository: likec4/likec4

Length of output: 672


🏁 Script executed:

# Test the correct fix: word boundary BEFORE 'is'
python - <<'PY'
import re

# Original regex (current) - no word boundary around 'is'
original = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(\s+not)?))?')

# Correct fix: word boundary BEFORE 'is'
correct_fix = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+\b(is)(?:\s+not)?\b)?')

tests = [
  'metadata.env',
  'metadata.env is prod',
  'metadata.env is not prod',
  'metadata.env island',
  'metadata.description is important',
]

print("ORIGINAL (false positive issue):")
for t in tests:
    m = original.search(t)
    result = m.group(0) if m else "None"
    print("  " + repr(t).ljust(40) + " -> " + repr(result))

print("\nCORRECT FIX (word boundary before 'is'):")
for t in tests:
    m = correct_fix.search(t)
    result = m.group(0) if m else "None"
    print("  " + repr(t).ljust(40) + " -> " + repr(result))
PY

Repository: likec4/likec4

Length of output: 786


🏁 Script executed:

# Check for similar "is" patterns without proper boundaries
grep -n "is" ./packages/vscode/likec4.tmLanguage.json | head -20

Repository: likec4/likec4

Length of output: 737


🏁 Script executed:

# Verify JSON formatting is valid
python - <<'PY'
import json
with open('./packages/vscode/likec4.tmLanguage.json') as f:
    content = json.load(f)
    print("✓ JSON is valid")
PY

Repository: likec4/likec4

Length of output: 72


Add word boundaries around the is keyword to prevent false-positive matches.

At Line 22, the is operator can be incorrectly matched as a prefix inside longer words (e.g., metadata.env island matches metadata.env is), causing incorrect syntax highlighting.

🔧 Suggested regex fix
-      "match": "\\b(metadata)\\.([a-zA-Z_]\\w*)(?:\\s+(is(\\s+not)?))?",
+      "match": "\\b(metadata)\\.([a-zA-Z_]\\w*)(?:\\s+\\b(is)(?:\\s+not)?\\b)?",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"match": "\\b(metadata)\\.([a-zA-Z_]\\w*)(?:\\s+(is(\\s+not)?))?",
"captures": {
"1": { "name": "keyword.control.likec4" },
"2": { "name": "variable.other.property.likec4" },
"3": { "name": "keyword.control.likec4" }
}
},
{
"match": "\\b(metadata)\\.([a-zA-Z_]\\w*)(?:\\s+\\b(is)(?:\\s+not)?\\b)?",
"captures": {
"1": { "name": "keyword.control.likec4" },
"2": { "name": "variable.other.property.likec4" },
"3": { "name": "keyword.control.likec4" }
}
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/likec4.tmLanguage.json` around lines 21 - 28, The regex in
the "match" field currently uses (is(\\s+not)?) which can match "is" as part of
longer words (causing false positives like "island"); update that capture to
enforce word boundaries around the "is" operator (for example replace the group
with a version that uses \\bis\\b and applies the optional "\\s+not" token with
its own word boundary), keeping the outer structure and the same capture index
for the "is" token so the captures 1/2/3 remain unchanged.

Copy link
Copy Markdown
Member

@davydkov davydkov left a comment

Choose a reason for hiding this comment

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

Hey @galuszkak
You did nice job, and everything looks pretty solid, but I'd like to run it locally first and check if code completions are not impacted (from the code it seems it should not)

In Claude you can also trigger changeset-generator skill - this helps with release notes

Can you also revert regenerated E2E screenshots? There is a github workflow to regenerate screenshots using same consistent env, but I don't expect any failures.

And yes, seems I need to update PR checks list.

@davydkov davydkov linked an issue Mar 17, 2026 that may be closed by this pull request
@galuszkak galuszkak force-pushed the feature/metadata-views-filtering branch from 1224211 to 01bf60b Compare March 17, 2026 09:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/language-server/src/like-c4.langium (1)

609-650: ⚠️ Potential issue | 🟡 Minor

Update TextMate grammars to support participant.metadata syntax.

The pnpm generate step appears complete (generated AST types are in use). However, TextMate grammars need updates for the new participant.metadata.key where clause syntax. Currently, all three grammar files only include the pattern for metadata.key, but the new grammar adds support for filtering by relationship participant metadata (e.g., source.metadata.env = production).

Add patterns to packages/vscode/likec4.tmLanguage.json, apps/docs/likec4.tmLanguage.json, and apps/playground/likec4.tmLanguage.json to recognize participant.metadata syntax in where clauses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/like-c4.langium` around lines 609 - 650, The
TextMate grammars are missing a pattern for the new participant.metadata
where-clause syntax introduced by WhereRelationParticipantMetadata (e.g.,
"participant.metadata.key = value"); update the three grammar files
(packages/vscode/likec4.tmLanguage.json, apps/docs/likec4.tmLanguage.json,
apps/playground/likec4.tmLanguage.json) to add a pattern that matches
"participant" followed by ".metadata" and a dotted key (Id) in the
where/relation context (analogous to the existing metadata.key pattern), assign
the same scopes used for metadata keys and operators so occurrences of
participant.metadata.<key> are highlighted like metadata.key, and ensure the
pattern is included in the where/relationship-related repository entries so
WhereRelationParticipantMetadata instances are recognized.
♻️ Duplicate comments (2)
packages/vscode/likec4.tmLanguage.json (1)

21-28: ⚠️ Potential issue | 🟡 Minor

Align the metadata matcher with the DSL grammar.

This regex still accepts invalid keys like metadata._1, misses valid ones like metadata.some-key, and can consume is from metadata.env island as if it were the operator. Tighten the identifier part to the DSL’s [_]*[a-zA-Z][-\w]* shape and put word boundaries around is. Please mirror the same fix in apps/docs/likec4.tmLanguage.json and apps/playground/likec4.tmLanguage.json.

🔧 Suggested fix
-      "match": "\\b(metadata)\\.([a-zA-Z_]\\w*)(?:\\s+(is(\\s+not)?))?",
+      "match": "\\b(metadata)\\.([_]*[a-zA-Z][-\\w]*)(?:\\s+\\b(is(?:\\s+not)?)\\b)?",

Run this to reproduce the current mismatches and compare the tightened pattern:

#!/bin/bash
python - <<'PY'
import re

current = re.compile(r'\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(\s+not)?))?')
fixed = re.compile(r'\b(metadata)\.([_]*[a-zA-Z][-\w]*)(?:\s+\b(is(?:\s+not)?)\b)?')

tests = [
    'metadata.env',
    'metadata.__env',
    'metadata.some-key',
    'metadata._1',
    'metadata.env is true',
    'metadata.env is not false',
    'metadata.env island',
]

for label, pattern in [('current', current), ('fixed', fixed)]:
    print(label.upper())
    for text in tests:
        match = pattern.search(text)
        print(f'  {text!r:<30} -> {match.group(0)!r if match else None}')
    print()
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/likec4.tmLanguage.json` around lines 21 - 28, Update the
"match" regex for the metadata rule so the identifier portion follows the DSL
shape and the "is" operator is bounded: replace the existing pattern
\b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(\s+not)?))? with a tightened pattern that
uses [_]*[a-zA-Z][-\w]* for capture group 2 and wraps the operator in word
boundaries (e.g. (?:\s+\b(is(?:\s+not)?)\b)?); apply this exact change to the
same "match" entry in the other two grammar files
(apps/docs/likec4.tmLanguage.json and apps/playground/likec4.tmLanguage.json) so
all three metadata matchers are consistent.
packages/language-server/src/lsp/SemanticTokenProvider.ts (1)

162-169: ⚠️ Potential issue | 🟠 Major

Drop ast.isWhereRelationKind from this grouped rule.

Line 150 already highlights WhereRelationKind.value as function(). Keeping it here emits a second token over the same range as type(), so relation-kind predicates get conflicting semantic tokens.

🔧 Suggested fix
     when(
-      isAnyOf(ast.isWhereElementTag, ast.isWhereElementKind, ast.isWhereRelationTag, ast.isWhereRelationKind),
+      isAnyOf(ast.isWhereElementTag, ast.isWhereElementKind, ast.isWhereRelationTag),
       mark => {
         if (isTruthy(mark.node.value)) {
           mark.property('value').readonly.definition.type()
         }
       },
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/lsp/SemanticTokenProvider.ts` around lines 162 -
169, Remove ast.isWhereRelationKind from the grouped predicate passed to isAnyOf
in the when(...) call so it no longer applies the same tokenization twice;
update the isAnyOf call to only include ast.isWhereElementTag,
ast.isWhereElementKind, and ast.isWhereRelationTag so that the
mark.property('value').readonly.definition.type() branch does not run for
WhereRelationKind nodes (which are already tokenized elsewhere).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/language-server/src/like-c4.langium`:
- Around line 609-650: The TextMate grammars are missing a pattern for the new
participant.metadata where-clause syntax introduced by
WhereRelationParticipantMetadata (e.g., "participant.metadata.key = value");
update the three grammar files (packages/vscode/likec4.tmLanguage.json,
apps/docs/likec4.tmLanguage.json, apps/playground/likec4.tmLanguage.json) to add
a pattern that matches "participant" followed by ".metadata" and a dotted key
(Id) in the where/relation context (analogous to the existing metadata.key
pattern), assign the same scopes used for metadata keys and operators so
occurrences of participant.metadata.<key> are highlighted like metadata.key, and
ensure the pattern is included in the where/relationship-related repository
entries so WhereRelationParticipantMetadata instances are recognized.

---

Duplicate comments:
In `@packages/language-server/src/lsp/SemanticTokenProvider.ts`:
- Around line 162-169: Remove ast.isWhereRelationKind from the grouped predicate
passed to isAnyOf in the when(...) call so it no longer applies the same
tokenization twice; update the isAnyOf call to only include
ast.isWhereElementTag, ast.isWhereElementKind, and ast.isWhereRelationTag so
that the mark.property('value').readonly.definition.type() branch does not run
for WhereRelationKind nodes (which are already tokenized elsewhere).

In `@packages/vscode/likec4.tmLanguage.json`:
- Around line 21-28: Update the "match" regex for the metadata rule so the
identifier portion follows the DSL shape and the "is" operator is bounded:
replace the existing pattern \b(metadata)\.([a-zA-Z_]\w*)(?:\s+(is(\s+not)?))?
with a tightened pattern that uses [_]*[a-zA-Z][-\w]* for capture group 2 and
wraps the operator in word boundaries (e.g. (?:\s+\b(is(?:\s+not)?)\b)?); apply
this exact change to the same "match" entry in the other two grammar files
(apps/docs/likec4.tmLanguage.json and apps/playground/likec4.tmLanguage.json) so
all three metadata matchers are consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b8b3edbc-a254-406e-af77-79710c9358d4

📥 Commits

Reviewing files that changed from the base of the PR and between 1224211 and 01bf60b.

⛔ Files ignored due to path filters (3)
  • e2e/tests/__screenshots__/chromium-darwin/e2e-amazon-s3.png is excluded by !**/*.png
  • packages/generators/src/model/__snapshots__/likec4.computed-model-with-core.snap is excluded by !**/*.snap
  • packages/generators/src/model/__snapshots__/likec4.computed-model.snap is excluded by !**/*.snap
📒 Files selected for processing (26)
  • apps/docs/likec4.tmLanguage.json
  • apps/docs/src/content/docs/dsl/Views/predicates.mdx
  • apps/playground/likec4.tmLanguage.json
  • examples/metadata-views/likec4.config.json
  • examples/metadata-views/views.c4
  • packages/core/src/compute-view/deployment-view/predicates/utils.ts
  • packages/core/src/compute-view/utils/buildComputedNodes.ts
  • packages/core/src/compute-view/utils/relationExpressionToPredicates.ts
  • packages/core/src/model/DeploymentElementModel.ts
  • packages/core/src/model/ElementModel.ts
  • packages/core/src/model/RelationModel.ts
  • packages/core/src/types/operators.spec.ts
  • packages/core/src/types/operators.ts
  • packages/core/src/types/view-computed.ts
  • packages/generators/src/likec4/operators/expressions.spec.ts
  • packages/generators/src/likec4/operators/expressions.ts
  • packages/generators/src/likec4/schemas/expression.ts
  • packages/language-server/src/__tests__/deployment-views.spec.ts
  • packages/language-server/src/__tests__/views-2.spec.ts
  • packages/language-server/src/formatting/LikeC4Formatter.ts
  • packages/language-server/src/like-c4.langium
  • packages/language-server/src/lsp/SemanticTokenProvider.ts
  • packages/language-server/src/model/model-parser-where.ts
  • packages/language-server/src/model/parser/Base.ts
  • packages/likec4/src/LikeC4.spec.ts
  • packages/vscode/likec4.tmLanguage.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/likec4/src/LikeC4.spec.ts
  • packages/language-server/src/tests/deployment-views.spec.ts
  • packages/core/src/compute-view/utils/buildComputedNodes.ts
  • packages/generators/src/likec4/operators/expressions.ts
  • packages/core/src/model/RelationModel.ts
  • packages/language-server/src/model/model-parser-where.ts
  • packages/generators/src/likec4/schemas/expression.ts
  • packages/core/src/types/view-computed.ts
  • packages/core/src/types/operators.spec.ts
  • examples/metadata-views/views.c4
  • apps/docs/src/content/docs/dsl/Views/predicates.mdx
  • packages/generators/src/likec4/operators/expressions.spec.ts
  • packages/language-server/src/model/parser/Base.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/metadata-views-filtering.md:
- Around line 2-3: The changeset .changeset/metadata-views-filtering.md
incorrectly marks the package bumps for '@likec4/core' and
'@likec4/language-server' as patch despite adding new feature functionality
(metadata filtering in view predicates); update the changeset to use minor
version bumps for both packages by changing their entries from 'patch' to
'minor' so the releases follow semantic versioning for backward-compatible
features.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b94e3c3-7097-43fa-a4b1-54120a7b0302

📥 Commits

Reviewing files that changed from the base of the PR and between 01bf60b and 0380c62.

📒 Files selected for processing (1)
  • .changeset/metadata-views-filtering.md

@galuszkak galuszkak force-pushed the feature/metadata-views-filtering branch from dc74c99 to 554bf8e Compare March 19, 2026 04:11
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/generators/src/likec4/schemas/expression.ts`:
- Around line 35-38: The whereMetadata schema currently restricts metadata.value
to strings via equalOp(z.string()).optional(), causing boolean predicates like
metadata.critical is true to be rejected; update the schema so
whereMetadata.value accepts both strings and booleans (e.g. replace
equalOp(z.string()) with equalOp(z.union([z.string(), z.boolean()])) or
equivalent) so the equalOp validation allows boolean metadata comparisons while
keeping the field optional.

In `@packages/likec4/src/LikeC4.spec.ts`:
- Around line 118-144: The current tests for views (using
model.view('prodOnly').elements(), 'excludeStaging', 'criticalOnly',
'prodNotDb') only assert subsets via toContain / not.toContain; change each
block to assert the exact set of element IDs by mapping elements to ids,
sorting, and comparing against the expected sorted array (e.g., assert that
prodOnlyIds sorted equals ['container.prodApi','container.prodDb'] and similarly
tighten excludeStaging, criticalOnly, and prodNotDb to exact expected ID arrays)
so over-inclusion will fail the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dce76dd8-4c8f-4c8d-b211-0650b84d95b9

📥 Commits

Reviewing files that changed from the base of the PR and between dc74c99 and 554bf8e.

⛔ Files ignored due to path filters (3)
  • e2e/tests/__screenshots__/chromium-darwin/e2e-amazon-s3.png is excluded by !**/*.png
  • packages/generators/src/model/__snapshots__/likec4.computed-model-with-core.snap is excluded by !**/*.snap
  • packages/generators/src/model/__snapshots__/likec4.computed-model.snap is excluded by !**/*.snap
📒 Files selected for processing (27)
  • .changeset/metadata-views-filtering.md
  • apps/docs/likec4.tmLanguage.json
  • apps/docs/src/content/docs/dsl/Views/predicates.mdx
  • apps/playground/likec4.tmLanguage.json
  • examples/metadata-views/likec4.config.json
  • examples/metadata-views/views.c4
  • packages/core/src/compute-view/deployment-view/predicates/utils.ts
  • packages/core/src/compute-view/utils/buildComputedNodes.ts
  • packages/core/src/compute-view/utils/relationExpressionToPredicates.ts
  • packages/core/src/model/DeploymentElementModel.ts
  • packages/core/src/model/ElementModel.ts
  • packages/core/src/model/RelationModel.ts
  • packages/core/src/types/operators.spec.ts
  • packages/core/src/types/operators.ts
  • packages/core/src/types/view-computed.ts
  • packages/generators/src/likec4/operators/expressions.spec.ts
  • packages/generators/src/likec4/operators/expressions.ts
  • packages/generators/src/likec4/schemas/expression.ts
  • packages/language-server/src/__tests__/deployment-views.spec.ts
  • packages/language-server/src/__tests__/views-2.spec.ts
  • packages/language-server/src/formatting/LikeC4Formatter.ts
  • packages/language-server/src/like-c4.langium
  • packages/language-server/src/lsp/SemanticTokenProvider.ts
  • packages/language-server/src/model/model-parser-where.ts
  • packages/language-server/src/model/parser/Base.ts
  • packages/likec4/src/LikeC4.spec.ts
  • packages/vscode/likec4.tmLanguage.json
✅ Files skipped from review due to trivial changes (8)
  • examples/metadata-views/likec4.config.json
  • .changeset/metadata-views-filtering.md
  • packages/language-server/src/tests/views-2.spec.ts
  • packages/core/src/types/operators.spec.ts
  • apps/docs/likec4.tmLanguage.json
  • packages/language-server/src/tests/deployment-views.spec.ts
  • examples/metadata-views/views.c4
  • packages/generators/src/likec4/operators/expressions.spec.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/core/src/model/RelationModel.ts
  • packages/language-server/src/model/parser/Base.ts
  • apps/playground/likec4.tmLanguage.json
  • packages/vscode/likec4.tmLanguage.json
  • packages/core/src/model/DeploymentElementModel.ts
  • packages/core/src/compute-view/deployment-view/predicates/utils.ts
  • packages/core/src/types/operators.ts
  • apps/docs/src/content/docs/dsl/Views/predicates.mdx

Comment on lines +35 to +38
export const whereMetadata = z.object({
metadata: z.object({
key: z.string(),
value: equalOp(z.string()).optional(),
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.

⚠️ Potential issue | 🟠 Major

Allow boolean metadata comparisons here.

This PR adds predicates like metadata.critical is true, but whereMetadata.value only validates strings. Generator inputs using boolean metadata filters will be rejected even though the rest of the stack now supports them.

🔧 Proposed fix
 export const whereMetadata = z.object({
   metadata: z.object({
     key: z.string(),
-    value: equalOp(z.string()).optional(),
+    value: equalOp(z.union([z.string(), z.boolean()])).optional(),
   }),
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generators/src/likec4/schemas/expression.ts` around lines 35 - 38,
The whereMetadata schema currently restricts metadata.value to strings via
equalOp(z.string()).optional(), causing boolean predicates like
metadata.critical is true to be rejected; update the schema so
whereMetadata.value accepts both strings and booleans (e.g. replace
equalOp(z.string()) with equalOp(z.union([z.string(), z.boolean()])) or
equivalent) so the equalOp validation allows boolean metadata comparisons while
keeping the field optional.

Comment on lines +118 to +144
// prodOnly: only prodApi and prodDb have environment=production
const prodOnlyElements = [...model.view('prodOnly').elements()]
const prodOnlyIds = prodOnlyElements.map(e => e.id)
expect(prodOnlyIds).toContain('container.prodApi')
expect(prodOnlyIds).toContain('container.prodDb')
expect(prodOnlyIds).not.toContain('container.stagingApi')

// excludeStaging: all except stagingApi
const excludeStagingElements = [...model.view('excludeStaging').elements()]
const excludeStagingIds = excludeStagingElements.map(e => e.id)
expect(excludeStagingIds).toContain('container.prodApi')
expect(excludeStagingIds).toContain('container.prodDb')
expect(excludeStagingIds).not.toContain('container.stagingApi')

// criticalOnly: only prodApi has critical=true
const criticalElements = [...model.view('criticalOnly').elements()]
const criticalIds = criticalElements.map(e => e.id)
expect(criticalIds).toContain('container.prodApi')
expect(criticalIds).not.toContain('container.stagingApi')
expect(criticalIds).not.toContain('container.prodDb')

// prodNotDb: production elements that are not database kind
const prodNotDbElements = [...model.view('prodNotDb').elements()]
const prodNotDbIds = prodNotDbElements.map(e => e.id)
expect(prodNotDbIds).toContain('container.prodApi')
expect(prodNotDbIds).not.toContain('container.prodDb')
expect(prodNotDbIds).not.toContain('container.stagingApi')
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.

⚠️ Potential issue | 🟡 Minor

Make these assertions exact.

Each case only proves a subset via toContain / not.toContain. If metadata filtering starts over-including elements, this test still passes. Compare the full sorted ID list for each view instead.

🧪 Example tightening for one case
-    const prodOnlyIds = prodOnlyElements.map(e => e.id)
-    expect(prodOnlyIds).toContain('container.prodApi')
-    expect(prodOnlyIds).toContain('container.prodDb')
-    expect(prodOnlyIds).not.toContain('container.stagingApi')
+    expect(prodOnlyElements.map(e => e.id).sort()).toEqual([
+      'container.prodApi',
+      'container.prodDb',
+    ])

Based on learnings: Applies to **/*.spec.ts : Aim to cover new features with relevant tests; keep test names descriptive

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/likec4/src/LikeC4.spec.ts` around lines 118 - 144, The current tests
for views (using model.view('prodOnly').elements(), 'excludeStaging',
'criticalOnly', 'prodNotDb') only assert subsets via toContain / not.toContain;
change each block to assert the exact set of element IDs by mapping elements to
ids, sorting, and comparing against the expected sorted array (e.g., assert that
prodOnlyIds sorted equals ['container.prodApi','container.prodDb'] and similarly
tighten excludeStaging, criticalOnly, and prodNotDb to exact expected ID arrays)
so over-inclusion will fail the test.

@davydkov davydkov enabled auto-merge March 19, 2026 09:18
@davydkov davydkov merged commit 597faab into likec4:main Mar 19, 2026
14 checks passed
@likec4-ci likec4-ci bot mentioned this pull request Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support metadata filtering in view predicates (where clause)

2 participants