ReadCap read filter: consume the lib's per-document model + validate against broker

Align Festipod's @data read-filter scenario and harness bridge with
ng-eventually's grant→ReadCap refactor: the access unit is the document
(an item's `@graph`), not the item.

- harness-ng.tsx: governDocument(reader, user)/setUser via getCaps()/resetCaps()
  (replaces setupReadFilter/setGrantOf); FilterProbe exposes a lazy snapshot()
  reflecting the current user without remount.
- read-filter.feature/steps: validate per-document ReadCap on the real
  DeepSignalSet — govern the wallet document, grant the cap to another user
  → current user sees 0; current user gets the cap → sees all (all-or-nothing
  in mono-store, the faithful behavior). 5/5 steps pass against the broker.
- doctrine: knowledge_stores-permissions records the verified store/document/
  repo/ReadCap model (containment by reference, no read-cap inheritance);
  decision_2026-06-17_eventually-library updates the access-rights + filter
  status to the ReadCap model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-06-29 11:20:12 +02:00
parent aec338441c
commit 073150ef61
6 changed files with 202 additions and 272 deletions
+29 -17
View File
@@ -12,7 +12,7 @@ import { createRoot } from 'react-dom/client';
import { NextGraphProvider, useNextGraph } from '../context/NextGraphContext';
import { FestipodDataProvider, useFestipodData } from '../context/FestipodDataContext';
import { useShape } from '@ng-eventually/client';
import { setGrantOf, setCurrentUser } from '@ng-eventually/client/polyfill';
import { getCaps, setCurrentUser, resetCaps } from '@ng-eventually/client/polyfill';
import type { DeepSignalSet } from '@ng-eventually/client';
import {
FpEventShapeType,
@@ -68,8 +68,9 @@ function ConnectedHarness() {
const participations = useShape(FpParticipationShapeType, privateNuri) as DeepSignalSet<FpParticipation>;
const [bridgeReady, setBridgeReady] = useState(false);
// Read-filter validation: when set, <FilterProbe> mounts a filtered useShape.
const [filterUser, setFilterUser] = useState<string | null>(null);
// Read-filter validation: once a ReadCap policy is active, <FilterProbe> mounts
// a useShape that returns the read-filtered VIEW.
const [filterActive, setFilterActive] = useState(false);
useEffect(() => {
// Small delay for useShape to populate
@@ -150,17 +151,26 @@ function ConnectedHarness() {
return bootstrapWallet(events as any, users as any, participations as any);
},
/** The document (repo NURI) all wallet entities live in (mono-store). */
documentNuri: privateNuri,
/**
* Enable the lib's READ FILTER on the real ORM set: each participation is
* granted to its own `user`, and the current user is `user`. <FilterProbe>
* then exposes window.__readFilter with the filtered participations.
* Put the wallet document under a ReadCap policy: grant its read cap to
* `reader` only, and set the current user to `user`. The lib's read
* filter is per-DOCUMENT, so this is all-or-nothing on that document —
* the faithful NextGraph behavior in a mono-store layout. <FilterProbe>
* then exposes window.__readFilter.snapshot() over the filtered view.
*/
setupReadFilter(user: string) {
setGrantOf((item: any) =>
item && item.user ? { read: [item.user], write: [item.user] } : undefined,
);
governDocument(reader: string, user: string) {
resetCaps();
getCaps().grantRead(privateNuri!, reader);
setCurrentUser(user);
setFilterActive(true);
},
/** Switch the current user (does the user now hold the document's cap?). */
setUser(user: string) {
setCurrentUser(user);
setFilterUser(user);
},
};
@@ -176,15 +186,16 @@ function ConnectedHarness() {
return (
<>
<div id="harness-status">{bridgeReady ? 'READY' : 'LOADING_SHAPES'}</div>
{filterUser && privateNuri && <FilterProbe privateNuri={privateNuri} />}
{filterActive && privateNuri && <FilterProbe privateNuri={privateNuri} />}
</>
);
}
// ============================================================================
// FilterProbe — subscribes participations AFTER the read filter is enabled, so
// useShape returns a filtered view. Exposes window.__readFilter for the @data
// scenario validating the read filter on the real ORM set.
// FilterProbe — subscribes participations AFTER a ReadCap policy is active, so
// useShape returns the read-filtered VIEW. Exposes window.__readFilter.snapshot()
// (evaluated lazily → reflects the CURRENT user) for the @data scenario that
// validates the per-document read filter on the real ORM set.
// ============================================================================
function FilterProbe({ privateNuri }: { privateNuri: string }) {
@@ -192,8 +203,9 @@ function FilterProbe({ privateNuri }: { privateNuri: string }) {
useEffect(() => {
(window as any).__readFilter = {
ready: true,
count: set.size,
users: [...set].map(p => p.user),
// Lazy: the filtered view reads the current user at access time, so calling
// snapshot() after setUser() reflects the new cap holder without remount.
snapshot: () => ({ count: set.size, users: [...set].map(p => p.user) }),
};
}, [set]);
return null;