Skip to content

feat: integrate scenario-specific relationships and attributes in schema files#2791

Open
285729101 wants to merge 4 commits intoPermify:masterfrom
285729101:feat/schema-scenario-refinement
Open

feat: integrate scenario-specific relationships and attributes in schema files#2791
285729101 wants to merge 4 commits intoPermify:masterfrom
285729101:feat/schema-scenario-refinement

Conversation

@285729101
Copy link

@285729101 285729101 commented Feb 17, 2026

Summary

  • Add Relationships and Attributes fields to the Scenario struct, enabling scenarios to define their own relationships and attributes in addition to global definitions
  • Process scenario-specific relationships and attributes before running checks in both the development engine (RunWithShape) and the CLI validate command
  • Update coverage calculations to aggregate global and scenario-specific data for accurate coverage reporting
  • Add test cases verifying scenario-specific relationships and attributes in coverage analysis
  • Update four example YAML schemas (custom-roles, banking-system, user-groups, organizations-hierarchies) to demonstrate the new capability

Test plan

  • All existing coverage tests pass (Cases 1-3)
  • New test Case 4 validates scenario-specific relationships are included in coverage calculations
  • New test Case 5 validates scenario-specific attributes are included in coverage calculations
  • Full go build ./... succeeds with no compilation errors
  • Manual testing with permify validate command against updated YAML files
  • Verify backward compatibility: existing YAML files without scenario-specific fields parse and work correctly

/claim #838

πŸ€– Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Support for defining attributes and relationships at the scenario level enables more granular test configuration
    • Enhanced coverage tracking now includes condition-based coverage metrics for permission schemas with detailed breakdown of covered and uncovered components
  • Tests

    • Added comprehensive test scenarios for scenario-specific relationships and attributes
    • Expanded test examples demonstrating updated validation and coverage capabilities

Allow scenarios to define their own relationships and attributes in
addition to global definitions, enabling more focused and contextual
testing. This addresses the need for dual flexibility: maintaining
the efficiency of global definitions while providing scenario-specific
configurations for complex test cases.

Changes:
- Add Relationships and Attributes fields to Scenario struct
- Process scenario-specific data before running checks in development.go
- Support scenario-specific data in CLI validate command
- Include scenario-specific data in coverage calculations
- Update example YAML schemas to demonstrate the new capability
- Add test cases for scenario-specific relationships and attributes

Closes Permify#838
…pecific data

Move task relationships to scenario-specific in custom-roles.yaml and
add a scenario-specific attribute example in banking-system.yaml.
Update calculateEntityCoverage to aggregate global and scenario-specific
relationships and attributes for coverage analysis. Also pass merged
data to condition-level coverage calculations.
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

πŸ“ Walkthrough

Walkthrough

This pull request introduces per-scenario relationships and attributes support across the validation and development frameworks. Changes include adding new fields to the Scenario struct, processing scenario-specific relationships and attributes during validation and development workflows, extending coverage tracking with condition-based metrics, and updating example YAML files to demonstrate the new functionality.

Changes

Cohort / File(s) Summary
Example Scenarios
assets/example-shapes/banking-system.yaml, assets/example-shapes/custom-roles.yaml, assets/example-shapes/organizations-hierarchies.yaml, assets/example-shapes/user-groups.yaml
Added new test scenarios demonstrating per-scenario relationships and attributes with corresponding permission assertions and checks.
Scenario Data Structure
pkg/development/file/shape.go
Added Relationships and Attributes fields to Scenario struct to support per-scenario relationship and attribute declarations.
Validation Processing
pkg/cmd/validate.go
Integrated per-scenario relationship and attribute processing into the scenario validation flow, converting, validating, and writing them to the database with error aggregation.
Development Flow
pkg/development/development.go
Extended RunWithShape to process scenario-specific relationships and attributes before checks, mirroring the validation flow with consistent error handling.
Coverage Infrastructure
pkg/development/coverage/coverage.go
Added permission condition coverage tracking with new exported types (ConditionCoverageInfo, ConditionComponent) and logic to calculate scenario-level coverage for conditions within per-entity coverage data.
Coverage Tests
pkg/development/coverage/coverage_test.go
Added two new test cases verifying coverage calculation for scenario-specific relationships and attributes across repository and account entities.

Sequence Diagram

sequenceDiagram
    participant Shape as Shape File
    participant Validator as Scenario Validator
    participant Processor as Relationship/Attribute Processor
    participant DB as Database
    participant Coverage as Coverage Tracker
    
    Shape->>Validator: Load scenario with relationships & attributes
    Validator->>Processor: For each scenario, process relationships
    Processor->>Processor: Convert to Tuple
    Processor->>Processor: Validate against schema
    Processor->>DB: Write tuple
    Processor->>Validator: Log success/failure
    
    Validator->>Processor: For each scenario, process attributes
    Processor->>Processor: Convert to Attribute
    Processor->>Processor: Validate against schema
    Processor->>DB: Write attribute
    Processor->>Validator: Log success/failure
    
    Validator->>Validator: Execute checks on scenario data
    Validator->>Coverage: Report covered relationships & attributes
    Coverage->>Coverage: Calculate condition coverage percentage
    Coverage->>Coverage: Track per-scenario coverage metrics
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Scenario-specific tales unfold,
Relationships and attributes bold,
Per-scenario data now takes the stage,
Coverage tracks each permission page,
Database writes sing in harmonious dance! ✨

πŸš₯ Pre-merge checks | βœ… 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% 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
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The PR title accurately describes the main feature: integrating scenario-specific relationships and attributes into schema files, which aligns with the changeset across example YAMLs, data structures, processing logic, and coverage calculations.

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

✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

@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

🧹 Nitpick comments (4)
assets/example-shapes/organizations-hierarchies.yaml (1)

49-58: Scenario logic is correct; minor style inconsistency with relationship quoting.

The permissions logic checks out β€” user:9999 as owner of repository:1234 correctly grants both edit and delete.

Nit: the scenario-specific relationship on line 52 uses a quoted string ("repository:1234#owner@user:9999"), while the global relationships on lines 26–27 are unquoted. Consider keeping them consistent for readability.

     relationships:
-      - "repository:1234#owner@user:9999"
+      - repository:1234#owner@user:9999
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/example-shapes/organizations-hierarchies.yaml` around lines 49 - 58,
The relationship in the scenario "owner_access_test" uses a quoted string
"repository:1234#owner@user:9999" which is inconsistent with the unquoted global
relationships; update the relationships entry for owner_access_test to use the
unquoted form repository:1234#owner@user:9999 (or alternatively quote all
relationship entries) so the formatting matches the global relationships while
preserving the same relation and subject.
assets/example-shapes/custom-roles.yaml (1)

29-29: Empty attributes key is acceptable but consider omitting or adding a comment.

The bare attributes: resolves to null in YAML. This works fine if the deserializer treats null as an empty list, but for an example file it may confuse users. Consider either removing it (if optional) or adding a short comment like # no global attributes.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/example-shapes/custom-roles.yaml` at line 29, The bare attributes: key
in the YAML example produces a null value and can confuse readers β€” either
remove the empty attributes: entry entirely if global attributes are optional,
or replace it with a short inline comment (e.g., "# no global attributes") so
the intent is clear; update the assets/example-shapes/custom-roles.yaml by
locating the attributes: key and applying one of these two fixes.
pkg/cmd/validate.go (1)

239-270: Same code duplication concern as development.go β€” extract shared helpers.

The parse β†’ validate β†’ write pattern for relationships and attributes is repeated here as well. A shared helper would eliminate duplication across both validate.go and development.go.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cmd/validate.go` around lines 239 - 270, Extract the repeated
parse→validate→write logic into a shared helper (e.g., processTuple or
processEntityTuple) and call it from both validate.go and development.go: the
helper should accept context, the tuple string (t), version, references to
dev.Container.SR and dev.Container.DW, serverValidation, and the aggregated
error list/color logger; inside it perform tuple.Tuple(t), call
ReadEntityDefinition (dev.Container.SR.ReadEntityDefinition) to get definition,
run serverValidation.ValidateTuple(definition, tup), attempt
dev.Container.DW.Write(...) and return any formatted error or success message so
the caller can add to list and print β€” replace the inlined blocks in the
for-loops with a call to this helper to remove duplication while preserving
identical error formatting and logging behavior.
pkg/development/development.go (1)

286-326: Consider extracting shared relationship/attribute processing logic to reduce duplication.

The parse β†’ read entity definition β†’ validate β†’ write β†’ collect error pattern is repeated four times: global relationships (lines 190–234), global attributes (lines 237–281), scenario relationships (lines 286–326), and scenario attributes (lines 329–369). Extracting into helpers like processRelationships(ctx, rels, version, errType, errKey) and processAttributes(...) would reduce ~160 lines of near-identical code.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/development/development.go` around lines 286 - 326, Duplicate
parse→readDefinition→validate→write error-handling logic for
relationships/attributes should be extracted into reusable helpers; create
helper functions (e.g., processRelationships(ctx, rels []string, version string,
errType string, errKey int) and processAttributes(...)) that perform
tuple.Tuple(), c.Container.SR.ReadEntityDefinition(ctx, "t1",
tup.GetEntity().GetType(), version),
validation.ValidateTuple/validation.ValidateAttribute, and
c.Container.DW.Write(ctx, "t1", database.NewTupleCollection(...) /
database.NewAttributeCollection()), and on error append the same Error struct to
the shared errors slice; replace the four duplicated blocks (global
relationships/attributes and scenario relationships/attributes that iterate over
scenario.Relationships/scenario.Attributes) with calls to these helpers passing
the proper rel/attr slice, version, "scenarios"/"globals" errType and the index
i or nil key as appropriate.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/development/coverage/coverage.go`:
- Around line 168-169: The append call using append(shape.Relationships,
scenario.Relationships...) can reuse shape.Relationships' backing array and
corrupt shared data; replace it with the copy-then-append pattern used
elsewhere: allocate a new slice sized to
len(shape.Relationships)+len(scenario.Relationships), copy(shape.Relationships,
...) into it, then copy or append scenario.Relationships into the new slice and
assign that to scenarioRelationships (do the same for scenarioAttributes with
shape.Attributes and scenario.Attributes). This ensures no reuse of the original
backing arrays when computing scenarioRelationships and scenarioAttributes in
calculateConditionCoverage.

In `@pkg/development/development.go`:
- Around line 285-369: The scenario loop is writing relationships/attributes
into the shared data writer (c.Container.DW) so scenario state accumulates
across iterations; to make scenarios isolated, reset or replace the data store
before each scenario: inside RunWithShape (the loop handling
scenario.Relationships and scenario.Attributes) either call the data-store
reset/truncate API on c.Container.DW (e.g., DW.Clear/Truncate/Reset if
available) or construct a fresh in-memory writer instance for each scenario and
use that for c.Container.DW.Write; update any tests or callers to use the
per-scenario DW and ensure validation
(validation.ValidateTuple/ValidateAttribute) still reads the correct entity
definitions, or if cumulative behavior is intended, update the function/method
documentation and naming to explicitly state that scenarios incrementally build
on prior data.

---

Duplicate comments:
In `@pkg/cmd/validate.go`:
- Around line 236-308: Scenario-specific relationships/attributes are being
written to the persistent DB under the hard-coded tenant "t1" via
dev.Container.DW.Write (using database.NewTupleCollection/
NewAttributeCollection) which causes them to persist across subsequent
scenarios; change the code to isolate scenario writes by using a
temporary/unique tenant or scoped transaction per scenario (e.g., derive a
scenario-specific tenant id from scenario name or open a DW transaction and
rollback/cleanup after the scenario) instead of writing to "t1", and ensure
cleanup of any temporary writes; also standardize error behavior or add a brief
comment in the loops handling scenario.Relationships and scenario.Attributes
explaining why those errors are soft-fail (list.Add/continue) if you intend to
keep that behavior.

---

Nitpick comments:
In `@assets/example-shapes/custom-roles.yaml`:
- Line 29: The bare attributes: key in the YAML example produces a null value
and can confuse readers β€” either remove the empty attributes: entry entirely if
global attributes are optional, or replace it with a short inline comment (e.g.,
"# no global attributes") so the intent is clear; update the
assets/example-shapes/custom-roles.yaml by locating the attributes: key and
applying one of these two fixes.

In `@assets/example-shapes/organizations-hierarchies.yaml`:
- Around line 49-58: The relationship in the scenario "owner_access_test" uses a
quoted string "repository:1234#owner@user:9999" which is inconsistent with the
unquoted global relationships; update the relationships entry for
owner_access_test to use the unquoted form repository:1234#owner@user:9999 (or
alternatively quote all relationship entries) so the formatting matches the
global relationships while preserving the same relation and subject.

In `@pkg/cmd/validate.go`:
- Around line 239-270: Extract the repeated parse→validate→write logic into a
shared helper (e.g., processTuple or processEntityTuple) and call it from both
validate.go and development.go: the helper should accept context, the tuple
string (t), version, references to dev.Container.SR and dev.Container.DW,
serverValidation, and the aggregated error list/color logger; inside it perform
tuple.Tuple(t), call ReadEntityDefinition
(dev.Container.SR.ReadEntityDefinition) to get definition, run
serverValidation.ValidateTuple(definition, tup), attempt
dev.Container.DW.Write(...) and return any formatted error or success message so
the caller can add to list and print β€” replace the inlined blocks in the
for-loops with a call to this helper to remove duplication while preserving
identical error formatting and logging behavior.

In `@pkg/development/development.go`:
- Around line 286-326: Duplicate parse→readDefinition→validate→write
error-handling logic for relationships/attributes should be extracted into
reusable helpers; create helper functions (e.g., processRelationships(ctx, rels
[]string, version string, errType string, errKey int) and
processAttributes(...)) that perform tuple.Tuple(),
c.Container.SR.ReadEntityDefinition(ctx, "t1", tup.GetEntity().GetType(),
version), validation.ValidateTuple/validation.ValidateAttribute, and
c.Container.DW.Write(ctx, "t1", database.NewTupleCollection(...) /
database.NewAttributeCollection()), and on error append the same Error struct to
the shared errors slice; replace the four duplicated blocks (global
relationships/attributes and scenario relationships/attributes that iterate over
scenario.Relationships/scenario.Attributes) with calls to these helpers passing
the proper rel/attr slice, version, "scenarios"/"globals" errType and the index
i or nil key as appropriate.

Comment on lines +168 to +169
scenarioRelationships := append(shape.Relationships, scenario.Relationships...)
scenarioAttributes := append(shape.Attributes, scenario.Attributes...)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe use of append on shared slice β€” may silently corrupt data across loop iterations.

append(shape.Relationships, scenario.Relationships...) reuses the backing array of shape.Relationships if it has spare capacity, meaning a later iteration can overwrite data written by an earlier one. While the result is currently consumed before the next iteration, this is a well-known Go footgun that will silently break if calculateConditionCoverage ever stores or returns the slice.

Use the same copy-then-append pattern already used on lines 142–146.

Proposed fix
-		scenarioRelationships := append(shape.Relationships, scenario.Relationships...)
-		scenarioAttributes := append(shape.Attributes, scenario.Attributes...)
+		scenarioRelationships := make([]string, len(shape.Relationships), len(shape.Relationships)+len(scenario.Relationships))
+		copy(scenarioRelationships, shape.Relationships)
+		scenarioRelationships = append(scenarioRelationships, scenario.Relationships...)
+
+		scenarioAttributes := make([]string, len(shape.Attributes), len(shape.Attributes)+len(scenario.Attributes))
+		copy(scenarioAttributes, shape.Attributes)
+		scenarioAttributes = append(scenarioAttributes, scenario.Attributes...)
πŸ“ 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
scenarioRelationships := append(shape.Relationships, scenario.Relationships...)
scenarioAttributes := append(shape.Attributes, scenario.Attributes...)
scenarioRelationships := make([]string, len(shape.Relationships), len(shape.Relationships)+len(scenario.Relationships))
copy(scenarioRelationships, shape.Relationships)
scenarioRelationships = append(scenarioRelationships, scenario.Relationships...)
scenarioAttributes := make([]string, len(shape.Attributes), len(shape.Attributes)+len(scenario.Attributes))
copy(scenarioAttributes, shape.Attributes)
scenarioAttributes = append(scenarioAttributes, scenario.Attributes...)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/development/coverage/coverage.go` around lines 168 - 169, The append call
using append(shape.Relationships, scenario.Relationships...) can reuse
shape.Relationships' backing array and corrupt shared data; replace it with the
copy-then-append pattern used elsewhere: allocate a new slice sized to
len(shape.Relationships)+len(scenario.Relationships), copy(shape.Relationships,
...) into it, then copy or append scenario.Relationships into the new slice and
assign that to scenarioRelationships (do the same for scenarioAttributes with
shape.Attributes and scenario.Attributes). This ensures no reuse of the original
backing arrays when computing scenarioRelationships and scenarioAttributes in
calculateConditionCoverage.

Comment on lines +285 to +369
// Write scenario-specific relationships if any are defined
for _, t := range scenario.Relationships {
tup, err := tuple.Tuple(t)
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("relationship: %s: %s", t, err.Error()),
})
continue
}

definition, _, err := c.Container.SR.ReadEntityDefinition(ctx, "t1", tup.GetEntity().GetType(), version)
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("relationship: %s: %s", t, err.Error()),
})
continue
}

err = validation.ValidateTuple(definition, tup)
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("relationship: %s: %s", t, err.Error()),
})
continue
}

_, err = c.Container.DW.Write(ctx, "t1", database.NewTupleCollection(tup), database.NewAttributeCollection())
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("relationship: %s: %s", t, err.Error()),
})
continue
}
}

// Write scenario-specific attributes if any are defined
for _, a := range scenario.Attributes {
attr, err := attribute.Attribute(a)
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("attribute: %s: %s", a, err.Error()),
})
continue
}

definition, _, err := c.Container.SR.ReadEntityDefinition(ctx, "t1", attr.GetEntity().GetType(), version)
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("attribute: %s: %s", a, err.Error()),
})
continue
}

err = validation.ValidateAttribute(definition, attr)
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("attribute: %s: %s", a, err.Error()),
})
continue
}

_, err = c.Container.DW.Write(ctx, "t1", database.NewTupleCollection(), database.NewAttributeCollection(attr))
if err != nil {
errors = append(errors, Error{
Type: "scenarios",
Key: i,
Message: fmt.Sprintf("attribute: %s: %s", a, err.Error()),
})
continue
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

rg -n 'reset\|rollback\|snapshot\|truncate\|clear\|delete.*tuple\|delete.*attribute' --type=go -g '!vendor/**'

Repository: Permify/permify

Length of output: 41


🏁 Script executed:

head -n 400 pkg/development/development.go | tail -n 150

Repository: Permify/permify

Length of output: 3919


🏁 Script executed:

fd -e _test.go | xargs grep -l "scenario" | head -5

Repository: Permify/permify

Length of output: 41


🏁 Script executed:

head -n 450 pkg/development/development.go | tail -n 200

Repository: Permify/permify

Length of output: 5371


🏁 Script executed:

head -n 300 pkg/development/development.go

Repository: Permify/permify

Length of output: 8152


🏁 Script executed:

rg -B5 -A5 'scenario.*reset\|reset.*scenario\|scenario.*isolat\|isolat.*scenario' --type=go pkg/development/

Repository: Permify/permify

Length of output: 41


Scenario-specific relationships and attributes accumulate across all scenarios without isolation.

The database writes scenario-specific relationships and attributes (lines 286–326 and 329–369) to the same in-memory store as global relationships/attributes. Since there is no reset or rollback mechanism between scenario iterations, data persists cumulatively. For example, if scenario 1 writes team:1#member@user:2, scenario 2 will see that tuple even if it doesn't declare it.

The comment "Each item in the Scenarios slice is processed individually" suggests independence, but the implementation contradicts thisβ€”all scenarios share a single database instance throughout the RunWithShape() call.

Clarify whether this cumulative behavior is intentional. If scenarios should be isolated, implement a reset mechanism (truncate tuples/attributes or use snapshots) between iterations. If cumulative behavior is intended, update the naming and documentation to reflect that scenarios build upon prior data.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/development/development.go` around lines 285 - 369, The scenario loop is
writing relationships/attributes into the shared data writer (c.Container.DW) so
scenario state accumulates across iterations; to make scenarios isolated, reset
or replace the data store before each scenario: inside RunWithShape (the loop
handling scenario.Relationships and scenario.Attributes) either call the
data-store reset/truncate API on c.Container.DW (e.g., DW.Clear/Truncate/Reset
if available) or construct a fresh in-memory writer instance for each scenario
and use that for c.Container.DW.Write; update any tests or callers to use the
per-scenario DW and ensure validation
(validation.ValidateTuple/ValidateAttribute) still reads the correct entity
definitions, or if cumulative behavior is intended, update the function/method
documentation and naming to explicitly state that scenarios incrementally build
on prior data.

@285729101 285729101 force-pushed the feat/schema-scenario-refinement branch from 4f103a6 to 486e85e Compare February 17, 2026 16:45
@285729101
Copy link
Author

@tolgaozen integrates scenario-specific relationships and attributes into schema files for better test coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments