Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .claude/rules/cross-app-mirrors-need-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
description: Hand-copied webapp logic in the companion (or any second app) must be marked as a mirror, point at its source, and prefer extraction to packages/*
globs: apps/companion/**
Comment thread
carlosvirreira marked this conversation as resolved.
---

# Cross-App Mirrors Need Provenance

The companion cannot import from `apps/webapp/app/**` (Remix-internal paths,
server-adjacent imports — Metro can't consume them). When it needs webapp
truth (permission matrices, enums, business constants), a hand-copied mirror
is sometimes the pragmatic choice — but every mirror MUST:

1. **Declare itself a mirror, never a source** — file-level JSDoc stating the
canonical file it mirrors and that the server enforces the real rules.
2. **Mirror the EFFECTIVE behavior, not the raw data** — e.g. the server's
`hasPermission()` short-circuits ADMIN/OWNER to allow-all; a copy of the
raw `Role2PermissionMap` alone is wrong. Say so in a comment at the spot.
3. **Be UI-cosmetic only** — if a client copy ever gates anything the server
does not independently enforce, that is a bug, not a mirror.
4. **Carry an extraction path** — when the mirrored thing is behavioral
(matrix + resolution logic), the durable fix is a shared workspace package
(`packages/*`, like `@shelf/database`). Note the intended package in the
JSDoc so reviewers see the debt is tracked, not accidental.

```ts
// ❌ Bad — silent copy; reviewer can't tell drift from design
const ROLE_PERMISSIONS = { OWNER: { qr: ["read", "update"] } };

// ✅ Good — provenance + effective-behavior note + extraction path
/**
* MIRROR of apps/webapp .../permission.data.ts — cosmetic UI gating only;
* server enforces via requireMobilePermission. Encodes the EFFECTIVE result
* (matrix + ADMIN/OWNER allow-all short-circuit). Extraction target:
* @shelf/permissions (see PR #2753 discussion).
*/
```

Existing mirrors: `apps/companion/lib/permissions.ts`. When you touch one,
diff it against its canonical source before shipping.
3 changes: 3 additions & 0 deletions apps/companion/app/(tabs)/assets/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export default function AssetsLayout() {
<Stack.Screen name="[id]" options={{ title: "Asset Details" }} />
<Stack.Screen name="new" options={{ title: "New Asset" }} />
<Stack.Screen name="edit" options={{ title: "Edit Asset" }} />
{/* Asset picker for linking a scanned (claimed, unlinked) QR code —
reached from the scanner's Unclaimed Code / No Asset Linked cards. */}
<Stack.Screen name="link-qr" options={{ title: "Link QR Code" }} />
{/* Kits live in this stack so the segmented Assets|Kits switcher swaps
lists in place and the tab bar keeps Assets active. */}
<Stack.Screen name="kits/index" options={{ title: "Kits" }} />
Expand Down
Loading
Loading