Skip to content

fix(policies): decode the capability mask when reopening a policy - #20

Merged
pyramation merged 2 commits into
mainfrom
fix/policy-capability-picker-decode-mask
Aug 10, 2026
Merged

fix(policies): decode the capability mask when reopening a policy#20
pyramation merged 2 commits into
mainfrom
fix/policy-capability-picker-decode-mask

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

metaschema_public.policy.data stores what the parser compiled, not what was authored:

{ "mask": "0000…00010001", "bitlen": 64, "membership_type": 2, }

rls_parser deletes capabilities/levels once they are folded into mask, so CapabilitySelectField — which reads the picker's value straight out of the policy data — reopened empty on every saved policy. The requirement was still enforced, but invisible: an operator couldn't see what a policy demanded, and selecting anything replaced the existing set rather than amending it.

The names are recoverable, so nothing needs to be persisted twice. Every catalog row owns one bit, so the mask is the selection:

export function decodeMask(mask: string, capabilities: CapabilityNode[], kind: CapabilityKind) {
  return capabilities
    .filter((c) => c.name && c.kind === kind && c.bitstr)
    .filter((c) => bitsIntersect(mask, c.bitstr))
    .map((c) => c.name);
}

bitsIntersect walks from the right, so a mask compiled before its module grew (17 bits) still lines up against a 64-bit catalog bitstr — bit numbering is right-anchored and lpad preserves it.

The decode is a fallback for the untouched state only, and isTouched keeps it from fighting the operator: without it, clearing the picker (which reports undefined, not []) would immediately snap back to the mask the form was opened with.

if (value) return value;                       // operator's selection
if (isTouched || typeof mask !== 'string') return [];
return decodeMask(mask, capabilitiesList, kind);

Mirrored into constructive-io/dashboard's vendored copy; unit tests cover the empty mask, the kind split, and the narrower-mask case.

Link to Devin session: https://app.devin.ai/sessions/8f89acc9280e4cef880921d966e0e8fc
Requested by: @pyramation

@pyramation pyramation self-assigned this Aug 10, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

A saved policy stores only the compiled mask, so the capability and level
pickers reopened empty and an operator could not see or amend the requirement
they had set. Decode the mask against the catalog instead: each row owns one
bit, so the names follow from the bits and nothing is persisted twice.
Caret ranges on 0.x majors do not cross minors, so `^0.5.0` stopped matching
once `@constructive-io/data` published 0.7.0 and the registry smoke install
failed to resolve it. Track the published minors and derive the contract test's
expectation from the constant.
@devin-ai-integration
devin-ai-integration Bot force-pushed the fix/policy-capability-picker-decode-mask branch from b83cbeb to df773db Compare August 10, 2026 05:19
@pyramation
pyramation merged commit 057ea6a into main Aug 10, 2026
6 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