Skip to content

Align Permission denial terminology#436

Merged
binaryfire merged 9 commits into
0.4from
refactor/permission-denied-terminology
Jul 15, 2026
Merged

Align Permission denial terminology#436
binaryfire merged 9 commits into
0.4from
refactor/permission-denied-terminology

Conversation

@binaryfire

@binaryfire binaryfire commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR aligns the Hypervel-specific explicit negative permission effect with the established authorization vocabulary of allow and deny.

  • Renames the public extension API from forbidden terminology to denied terminology.
  • Renames the assignment effect column and every hydrated or serialized representation from is_forbidden to is_denied.
  • Updates internal helpers, synchronization state, cache payloads, tests, package documentation, framework documentation, and active design plans as one coherent migration.
  • Preserves every Spatie-compatible API and keeps explicit denial as an additive Hypervel capability.
  • Removes legacy names rather than retaining aliases, fallback reads, dual fields, or transition machinery.

For more details, see docs/plans/2026-07-15-permission-denied-terminology.md

Motivation

Hypervel Auth already uses allow and deny throughout Gate responses and authorization decisions. Permission previously paired an allowed synchronization input with forbidden assignment terminology, which made the public API and storage model less intuitive than the underlying behavior.

The permission edge has two effects: it either allows or explicitly denies an ability. Denied is the conventional and direct name for that state. It also makes the precedence rule clear: an explicit deny wins over a direct or role-granted allow.

This is a terminology migration, not a new authorization model. The existing single-row assignment representation, precedence behavior, cache architecture, invalidation rules, events, and query shapes remain intact.

Public API

The Hypervel extension now exposes:

  • denyPermissionTo(...$permissions)
  • hasDeniedPermission($permission, $guardName = null)
  • hasDeniedPermissionViaRoles($permission, $guardName = null)
  • syncPermissionEffects(allowed: [...], denied: [...])
  • PermissionRegistrar::hasDeniedRolePermissions()

The Spatie-compatible surfaces remain unchanged, including givePermissionTo(), revokePermissionTo(), syncPermissions(), role assignment APIs, relations, scopes, middleware, Gate integration, and events.

The removed names are covered by reflection assertions so compatibility aliases cannot be reintroduced accidentally.

Schema and cache contract

Both effect-bearing pivots now use a boolean is_denied column:

  • model_has_permissions
  • role_has_permissions

The same name is used by relation pivots, model assignment cache entries, serialized role-permission edges, and hydrated catalog state. There is no translation layer between storage and runtime terminology.

The catalog hasDeniedRolePermissions field and direct-assignment is_denied field are required package-generated cache data. The previous fail-open fallbacks treated a missing effect as false; those fallbacks are removed so malformed payloads cannot silently suppress an explicit denial check.

Regression coverage exercises both real cache paths:

  • normal permission catalog serialization, invalidation, and hydration;
  • a cold model-assignment cache hydration where a direct deny must override a competing role allow without loading the relation shortcut.

Behavior and performance

Authorization semantics are unchanged:

  • direct denied assignments override direct and inherited allows;
  • denied role assignments override direct and other-role allows;
  • assigning the opposite effect updates the existing edge;
  • denied input wins when the same permission appears in both synchronization lists;
  • revocation removes the assignment edge regardless of its effect.

The implementation adds no database query, cache operation, hot-path branch, or synchronization write. SQL uses the same predicates and joins with the renamed physical column. Permission synchronization retains its pivot-only state read, bulk detach and attach behavior, and at most two batched effect updates.

Event classes, payloads, dispatch timing, listener guards, cache identities, invalidation, guards, teams, generic row partitions, wildcard checks, coroutine memoization, and integer, UUID, and ULID key behavior are unchanged.

Documentation

The package README and Boost Permission guide now describe explicit denied assignments, effect flipping, precedence, synchronization, inspection, and revocation using one vocabulary. Active Permission design plans were updated so future implementation work does not learn stale symbols.

Historical _archive content and unrelated HTTP 403 terminology remain untouched.

Testing

composer fix passes in full:

  • PHP CS Fixer: 0 files changed
  • PHPStan main configuration: no errors
  • PHPStan split configuration: no errors
  • Components: 22,735 tests, 64,620 assertions, 1,585 skipped
  • Testbench package suite: 327 tests, 955 assertions, 3 skipped
  • Dogfood package suite: 4 tests, 7 assertions

Focused coverage also verifies direct and inherited denial, Gate behavior, wildcard checks, guards, teams, generic row partitions, coroutine isolation, custom keys and schemas, cache invalidation, query counts, synchronization batching, UUID database integration, events, and removed API names.

Summary by CodeRabbit

  • New Features

    • Added denied-permission APIs for assigning, checking, and synchronizing permission effects.
    • Denied permissions now consistently override allowed permissions, including role, wildcard, guard, team, and scope checks.
    • Added support for denied-state tracking in permission storage and caching.
  • Breaking Changes

    • Replaced forbidden-permission terminology and APIs with denied-permission equivalents.
    • Renamed the permission effect field to is_denied.
    • Removed legacy forbidden-permission methods and configuration references.
  • Documentation

    • Updated permission guides, examples, migration guidance, and feature labels to describe denied permissions.

Use the framework-standard denied terminology in the permission package's about output.

Update the command coverage alongside the provider so the public feature summary remains tested as one coherent change.
Rename Hypervel Permission's explicit negative assignment effect from forbidden to denied across the public extension API, pivot schema, relations, authorization checks, synchronization internals, and cache payloads.

Keep Spatie-compatible APIs and behavior intact while exposing denyPermissionTo, hasDeniedPermission, hasDeniedPermissionViaRoles, syncPermissionEffects, and hasDeniedRolePermissions as coherent Hypervel additions.

Treat both cached effect fields as required package-generated data instead of silently failing open when malformed payloads omit them. Cover real catalog and model-assignment cache round trips, preserve query counts and batched writes, and add a reflection guard proving the removed method names cannot return as compatibility aliases.

Migrate the complete Permission test surface, including schemas, custom keys, events, guards, teams, partitions, coroutine isolation, UUID paths, query counts, and the renamed denied-permission behavior suite.
Describe explicit denied assignments using the same allow and deny vocabulary as Hypervel Auth.

Document the new Hypervel extension APIs, the single-edge is_denied representation, effect flipping, and the distinction between effective allowed collections and explicit denied-edge inspection.
Update the Permission guide's navigation, schema examples, assignment APIs, synchronization examples, cache discussion, partition examples, and Spatie differences to use denied terminology consistently.

Clarify that a denied assignment is an explicit edge effect, that it overrides allowed edges, and that revocation removes either effect without changing the package's broader authorization semantics.
Bring the original Spatie port plan forward to the package's final denied vocabulary so it no longer teaches stale APIs, pivot columns, cache fields, or test names.

Retain the plan's historical design intent while describing the current single-edge effect model and settled Hypervel extension surface accurately.
Rename the single-state hardening plan and update its complete design record to use denied terminology for public APIs, internal helpers, pivot state, cache payloads, tests, and acceptance criteria.

Preserve the original correctness and performance decisions while ensuring active design documentation presents only the current vocabulary.
Replace stale forbidden-effect symbols and prose throughout the Permission hardening and performance review plan.

Keep its guard, cache-indexing, wildcard, event, and performance conclusions unchanged while making every code example and test reference match the live denied API.
Align the row-partitioning design with the denied permission API, is_denied pivot schema, synchronization vocabulary, cache aggregate, and current test names.

Leave the partition architecture, query-count guarantees, lifecycle behavior, and ownership boundaries unchanged.
Record the authorization-vocabulary research, settled public API, schema and cache contracts, runtime invariants, file-by-file migration, testing strategy, exclusions, and acceptance criteria for the denied terminology change.

Document the behavior-preserving and zero-overhead constraints, required cache fields, greenfield removal of legacy names, and the distinction between Permission effects and unrelated HTTP or ordinary-English uses.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The permission model and documentation are renamed from forbidden to denied. Pivot schemas, runtime checks, synchronization, cache serialization, public APIs, partition/team behavior, and tests now use is_denied, denied-specific methods, and deny-overrides-allow semantics.

Changes

Permission denial migration

Layer / File(s) Summary
Denied permission contracts and plans
docs/plans/*
Plans define single-state denied edges, denied-first evaluation, synchronization, caching, partitioning, and terminology requirements.
Denied storage, evaluation, and synchronization
src/permission/src/*, src/permission/database/migrations/*
Relations, pivot fields, runtime checks, synchronization, and catalog serialization use is_denied and denied-specific APIs.
Denied API and schema documentation
src/permission/README.md, src/boost/docs/permission.md
Documentation describes denied permissions, denied APIs, and the updated schema and synchronization examples.
Denied behavior and compatibility validation
tests/Permission/*, tests/Integration/Database/*
Tests cover denied checks, deny-wins synchronization, caching, guards, partitions, teams, events, scopes, and removed legacy methods.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: renaming permission negative-effect terminology from forbidden to denied.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/permission-denied-terminology

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.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR is a clean, coherent terminology migration across the entire permission package — renaming the is_forbidden / forbidden vocabulary to is_denied / denied in source, tests, and documentation simultaneously. The public extension API (denyPermissionTo, hasDeniedPermission, syncPermissionEffects), schema columns, cache payloads, and internal helpers are all renamed in one pass without leaving any aliases or dual-field reads.

  • Schema: The initial creation migration updates both pivot columns to is_denied, but no separate rename migration is provided for installations that already have the old column. Without one, all withPivot('is_denied') loads, WHERE is_denied predicates, and pivot->getAttribute('is_denied') reads silently yield null/false on existing databases, causing all explicitly-denied permissions to appear allowed.
  • Cache contract: The ?? false safety nets on $payload['hasDeniedRolePermissions'] and $assignment['is_denied'] were removed, and the companion backward-compatibility test was replaced with a functional test that does not cover stale cache entries. Any warm cache entry written by the previous code version will produce PHP undefined-array-key warnings and suppress denial checks until the entry expires.
  • Everything else (API surface, event payloads, query shapes, coroutine isolation, wildcard and team paths, sync logic, and documentation) is consistently and correctly renamed.

Confidence Score: 3/5

Not safe to merge without a column rename migration for existing installations; upgrading users will lose all explicitly-denied permission enforcement silently.

The terminology rename itself is thorough and internally consistent across source, tests, and documentation. The blocking gap is the missing database migration: the initial creation migration was updated to use is_denied, but any project that already ran the old migration retains the is_forbidden column. On those installations, every call to withPivot('is_denied'), WHERE is_denied, and getAttribute('is_denied') returns null, so all denied permissions are silently treated as allowed. The cache transition carries a related issue — stale cache entries with the old key names emit PHP warnings and return false for denial checks — which affects zero-downtime deployments that don't flush the cache atomically with the code push.

src/permission/database/migrations/2025_07_02_000000_create_permission_tables.php needs a companion rename migration; src/permission/src/Traits/HasPermissions.php and src/permission/src/PermissionRegistrar.php need attention for the stale-cache deserialization paths.

Important Files Changed

Filename Overview
src/permission/database/migrations/2025_07_02_000000_create_permission_tables.php Renames is_forbidden to is_denied in the creation migration only; no rename migration is provided for existing installations, leaving the schema incompatible with the updated code.
src/permission/src/Traits/HasPermissions.php Comprehensive rename of all is_forbidden/forbidden identifiers to is_denied/denied; the removed ?? false fallback on the cache-deserialization path leaves stale model-assignment entries producing PHP warnings and suppressing denial checks during the transition window.
src/permission/src/PermissionRegistrar.php Clean rename of hasForbiddenRolePermissions to hasDeniedRolePermissions and pivotIsForbidden to pivotIsDenied; direct array access without a fallback emits PHP warnings on stale cache and the protective backward-compatibility test was removed.
src/permission/src/Models/Permission.php Updates withPivot call from is_forbidden to is_denied; straightforward one-line change.
src/permission/src/Models/Role.php Renames withPivot, hasForbiddenPermission, and pivotIsForbidden references to the new terminology; clean and consistent.
src/permission/src/PermissionServiceProvider.php Renames the about label from Forbidden Permissions to Denied Permissions; trivial cosmetic change.
tests/Permission/DeniedPermissionTest.php New test file renamed from ForbiddenPermissionTest; comprehensive coverage of direct and role-based denial, flip behavior, guard scoping, sync operations, and scopes.
tests/Permission/CacheTest.php Renames cache test methods and adds a new direct-denied pivot hydration test; the removed stale-payload backward-compatibility test is not replaced, leaving the cache-transition window untested.
tests/Permission/Integration/PermissionRegistrarTest.php Replaces the backward-compatibility test for a missing cache key with a simple functional test; the transition scenario (stale cache from old code) is no longer covered.
tests/Permission/PublicApiTest.php Updates parameter assertions for renamed methods and adds reflection-based assertions that verify removed methods no longer exist on the public API.

Comments Outside Diff (1)

  1. src/permission/src/PermissionRegistrar.php, line 118 (link)

    P1 Stale cache entries emit PHP warnings and suppress denial checks

    The ?? false safety net was removed, so any cache entry serialized by the previous code version (which stored hasForbiddenRolePermissions instead of hasDeniedRolePermissions) causes PHP to emit an E_WARNING for an undefined array key and evaluate (bool) null as false. The downstream effect is identical to the old fallback — hasDeniedRolePermissions() returns false, causing hasDeniedPermissionViaRoles() to short-circuit and skip the per-permission denial check — but now with warnings in the error log. The companion test (testOldCachePayloadWithoutForbiddenRolePermissionFlagIsSafe) that verified the graceful-degradation path has been removed without a replacement. On zero-downtime deployments where the shared cache is not flushed atomically with the code push, any warm role-permission cache from the old version silently suppresses denials until the cache TTL expires.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

Reviews (1): Last reviewed commit: "Add the Permission denied terminology im..." | Re-trigger Greptile

Comment thread src/permission/src/Traits/HasPermissions.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/permission/src/Traits/HasPermissions.php (1)

1760-1763: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use an evidence-based union type instead of mixed.

Database driver implementations can return booleans as bool, int, string, or null. As per coding guidelines, use strict typing, modern PHP types, and evidence-based union types instead of mixed when all valid types are known.

♻️ Proposed refactor
     /**
      * Normalize a permission assignment effect.
      */
-    protected function permissionEffectIsDenied(mixed $value): bool
+    protected function permissionEffectIsDenied(bool|int|string|null $value): bool
     {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/permission/src/Traits/HasPermissions.php` around lines 1760 - 1763,
Update the parameter type of permissionEffectIsDenied() from mixed to the
evidence-based union bool|int|string|null, preserving the method’s existing
behavior and return type.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/permission/src/Traits/HasPermissions.php`:
- Around line 1760-1763: Update the parameter type of permissionEffectIsDenied()
from mixed to the evidence-based union bool|int|string|null, preserving the
method’s existing behavior and return type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a9511d8a-5830-4be8-9dd5-07d655382a41

📥 Commits

Reviewing files that changed from the base of the PR and between 7317f53 and 601150d.

📒 Files selected for processing (33)
  • docs/plans/2026-06-24-2406-permission-fresh-spatie-port.md
  • docs/plans/2026-07-02-permission-denied-single-state-hardening.md
  • docs/plans/2026-07-02-permission-review-hardening-and-performance.md
  • docs/plans/2026-07-13-permission-row-partitioning.md
  • docs/plans/2026-07-15-permission-denied-terminology.md
  • src/boost/docs/permission.md
  • src/permission/README.md
  • src/permission/database/migrations/2025_07_02_000000_create_permission_tables.php
  • src/permission/src/Models/Permission.php
  • src/permission/src/Models/Role.php
  • src/permission/src/PermissionRegistrar.php
  • src/permission/src/PermissionServiceProvider.php
  • src/permission/src/Traits/HasPermissions.php
  • tests/Integration/Database/PermissionPartitionTest.php
  • tests/Permission/CacheTest.php
  • tests/Permission/Commands/CommandTest.php
  • tests/Permission/CustomSchemaConfigTest.php
  • tests/Permission/DeletionTest.php
  • tests/Permission/DeniedPermissionTest.php
  • tests/Permission/Events/EventTest.php
  • tests/Permission/GateTest.php
  • tests/Permission/Integration/PartitionQueryCountTest.php
  • tests/Permission/Integration/PermissionRegistrarTest.php
  • tests/Permission/PartitionAuthorizationTest.php
  • tests/Permission/PartitionCoroutineIsolationTest.php
  • tests/Permission/PartitionRelationsTest.php
  • tests/Permission/PartitionTeamsTest.php
  • tests/Permission/PartitionTestCase.php
  • tests/Permission/PublicApiTest.php
  • tests/Permission/SchemaConfigTest.php
  • tests/Permission/TestCase.php
  • tests/Permission/Traits/HasPermissionsTest.php
  • tests/Permission/Traits/TeamHasPermissionsTest.php

@binaryfire

Copy link
Copy Markdown
Collaborator Author

@greptile-apps @coderabbitai Thanks for the review. I traced each finding through the release history, schema/query behavior, cache producers and hydrators, and PDO/Eloquent boundaries. No follow-up code change is needed.

  • Rename migration: The explicit-denial feature has not shipped; is_forbidden exists in zero of the repository’s 57 tags. A rename migration would preserve an unreleased intermediate schema rather than support an installed release. The creation migration should define the final is_denied schema directly. Also, a missing column fails loudly at the SQL layer (SELECT / WHERE is_denied raises an unknown-column error); it does not silently hydrate every edge as allowed.
  • Catalog and model-assignment cache compatibility: No released version produced cache payloads with the former keys. Legacy-key reads would therefore be dead compatibility code. Restoring ?? false would also weaken the invariant for security-relevant package-generated data: an invalid payload missing its denial field would fail open. The final payload requires hasDeniedRolePermissions and is_denied, with real producer/hydrator coverage for both paths.
  • permissionEffectIsDenied() parameter: mixed is intentional. The value crosses genuine Eloquent attribute and PDO fetch boundaries, and application PDO options may enable ATTR_STRINGIFY_FETCHES, making string values reachable. Enumerating bool|int|string|null would imply an exhaustive runtime guarantee that the upstream mixed boundary does not provide. The method immediately normalizes the boundary value to bool.
  • Docstring coverage: This diff is test-heavy, and this repository does not add generated restatement docblocks to test methods. The affected production methods already use the project-standard Laravel-style method docblocks. Raising the metric mechanically would add noise without improving the API contract.

The final branch remains unchanged. A fresh Permission suite run passes with 979 tests and 2,617 assertions, and the worktree is clean.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@binaryfire Thanks for the thorough trace-through — this all makes sense and aligns with how the package is versioned and how the security-relevant cache invariants should behave. Appreciate you confirming with a full suite run (979 tests, 2,617 assertions) and a clean worktree.

(੭ ˃̀ ᵥ ˂̀)੭ good to have this documented for future passes


✏️ Learnings added
Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/436

Timestamp: 2026-07-15T17:08:48.214Z
Learning: In the Hypervel Permission package (src/permission), the `is_forbidden` pivot/schema field and its associated "forbidden" terminology never shipped in any released tag (0 of 57 repository tags). Because of this, schema/cache changes renaming `is_forbidden`/`forbidden` to `is_denied`/`denied` do not require rename migrations or backward-compatibility fallback reads (e.g. `?? false`) for legacy cache payload keys like `hasForbiddenRolePermissions` or `is_forbidden` — these would be dead compatibility code for a schema that was never installed. The creation migration should define the final `is_denied` column directly.

Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/436

Timestamp: 2026-07-15T17:08:48.214Z
Learning: In the Hypervel Permission package, cache payloads for the permission catalog and model-assignment cache are security-relevant, package-generated data. Fields like `hasDeniedRolePermissions` and `is_denied` are intentionally required in the payload shape with no `?? false` fallback, so that an invalid/incomplete cached payload fails loudly rather than failing open (i.e., silently treating missing denial data as "allowed"). Real producer/hydrator test coverage is added for these fields instead of adding legacy-key compatibility reads.

Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/436

Timestamp: 2026-07-15T17:08:48.214Z
Learning: In the Hypervel Permission package's `HasPermissions` trait, the internal helper `permissionEffectIsDenied()` intentionally accepts a `mixed` parameter type rather than a narrower union like `bool|int|string|null`. This is because the value crosses genuine Eloquent attribute and PDO fetch boundaries, and PDO options such as `ATTR_STRINGIFY_FETCHES` can make string values reachable at runtime; the method immediately normalizes the value to `bool` internally.

Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/436

Timestamp: 2026-07-15T17:08:48.214Z
Learning: In the Hypervel Permission package's test suites (e.g. tests/Permission/*), test methods intentionally do not have generated restatement docblocks — this is a project convention. Production methods use standard Laravel-style method docblocks, but adding mechanical docblocks to test methods is considered noise rather than an improvement to the API contract.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

All four justifications hold up against the implementation details.

On the migration: zero released tags containing is_forbidden means the only installed schema is the one created by this PR's migration. A rename migration would anchor the upgrade path to an intermediate state that no one ever ran. The creation migration defining is_denied directly is correct, and the SQL-level failure on an unknown column is the right failure mode — loud and immediate rather than silent.

On cache compatibility: dead compatibility code for keys no released producer ever emitted is the right call to avoid. The fail-open argument is the stronger point here — ?? false on a security-relevant field means a malformed or partially-written cache entry silently suppresses an explicit denial. Requiring the field and letting the absence surface as an error is the safer invariant for that boundary.

On mixed for permissionEffectIsDenied(): the ATTR_STRINGIFY_FETCHES case is exactly right. That flag is a PDO-level behavior that can be set at connection configuration time, outside the package's control, making string "1" and "0" genuinely reachable at that boundary. Declaring a narrower union type would be a false contract. Immediate normalization to bool inside the method is the correct pattern.

On docstring coverage: if the project convention is no restatement docblocks on test methods and production methods already carry Laravel-style docblocks, adding them to satisfy a metric serves no contract purpose.

No follow-up changes needed. The implementation is consistent and the reasoning is sound.

@binaryfire
binaryfire merged commit 16a1226 into 0.4 Jul 15, 2026
34 of 36 checks passed
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.

1 participant