graview-sight

graview-sight is one of Graview's 31 authoring skills: instructions an AI coding assistant such as Claude Code or Codex reads before it changes a Graview app. What it teaches: Say who sees which records of a Graview app, and which of their fields — so a seat is served only what it may see, on every surface at once — verified by graview check and by comparing what a seat is told with a hidden value and without one.

Installed into a project by graview skills install ., and read by whichever assistant is working beside you.

The skill

Say who sees what

A grant says who may DO an act; a sight says who may SEE a record, and which of its fields. It is the same kind of declaration — once, in the policy — and the store is what enforces it: every surface a seat reads (the scene, the pages, Find, the log, an agent's tools, the wire) is handed the seat's view, store.seenBy(principal), so none of them has to remember.

Do this

  1. Say it for every kind. sees keeps records from seats the way grants keep acts: once there is one sight, a kind no sight names is seen by nobody but the system (sight-unnamed-kind says which).

``ts sees: [ { roles: "*", kinds: ["car"] }, // the shop window { roles: ["shopper"], kinds: ["test-drive"], own: true }, // theirs, joined to theirs, or made by them { roles: ["participant"], kinds: ["match"], via: "givenBy" }, // only what that relation joins to their own record ] ``

A served store sends a seat only what it sees, withholds the rest of the log in place, and refuses an act that names a record it may not see exactly as one that names nothing — a document's setsOther target too; an act's setsLinked reaches only the linked records the seat sees, and is refused, naming the record, where one of them is not the seat's to change. A record whose required reference names a kind its seat may not see is withheld whole (sight-hides-required-ref): let the role see the target, or make the field optional.

  1. Hide a field, not only a kind. A roster where parents see every player's name and not their phone number:

``ts sees: [ { roles: ["coach"], kinds: ["player"] }, { roles: ["parent"], kinds: ["player"], hide: ["phone", "medical"] }, { roles: ["parent"], kinds: ["player"], own: true }, // their own child, whole ] ``

hide keeps those fields; fields: ["name"] is an allow list, so a field added tomorrow is kept until a sight shows it. SIGHTS ADD UP: a field is shown when any sight that shows the seat the record shows it, so the own sight gives a parent their own child's phone back, and a person with two roles sees what either shows. A kept field is not in anything the seat is served — the record, the log (an op that wrote it, or whose sentence read it, is withheld), a search, a name, a form, an import — and the seat may not write it: the act is refused as one given an argument it does not take, whatever the field held. A computed field and a rule are worked out over what the seat is served, so to a parent a phone it may not see reads as none.

  1. Load the judge where the store is. Field sight is @graview/core/field-sight: @graview/ship's server entry loads it. A page that holds the whole store (openStore from @graview/ship/browser, or a Store of its own) imports it once — import "@graview/core/field-sight";, as apps/gauntlet/src/main.tsx does — and so does a host that serves seats without @graview/ship. Without it a sight with hide or fields shows nothing: the store keeps the record rather than serve a field it cannot judge — a maker's own record too, so { roles: "*", kinds: ["ticket"], own: true, hide: ["triage"] } that shows a sender nothing means the import is missing; loaded, it shows them their ticket without triage. A page openRemote opened needs nothing: its host judged what it was sent, and that store alone is trusted — a whole store on the same page still needs the import. An agent's tool for an act lists no argument writing a field the seat never sees.
  1. Edit it as a document. add-sight and remove-sight take hide and fields like any other key; a field none of the sight's kinds has is refused (sight-field).
  1. A notification is a read too. "Notify me" (@graview/core/subscriptions) judges each commit as the subscriber's seat: a change to a record they cannot see never matches, a field kept from them is neither said in the match nor read by its condition, and a change withheld from them says "Someone". Prove it as above: matchSubscriptions(store, ops, [sub]) for a world with the hidden record answers [] (packages/core/tests/unit/a-subscription-is-told-what-its-subscriber-may-see.test.ts).
  1. What a seat reads beside a record keeps to its sight too. - An id is served wherever its record is, so it is never minted from a field some sight keeps from a role: a tip whose title the public may not see is tip:tip-2, not tip:secret-merger-plan — an act, an import, and a day of a series changed apart alike. An act of your own gets the same from freshId(name, kind, record?) when the name quotes an argument for such a field, or a field of the record handed third; a name it builds from anything else is yours to keep clean. - A record withheld whole — its required reference names a kind the seat may not see — is refused as one that is not there, whatever the grant says, exactly as an id that names nothing. - The app's own actor's suggestions say why in words that quote fields; a seat whose sight keeps one the basis read is served the suggestion, and the change it made, withheld. - A sight with hide or fields shows a kind in part, so even a roles: "*" one is not total sight for a worker view: the view's own acts need a press the host saw.

One sign-in, several people at the screen

A host says who at a shared screen on the seat: principal: { ..., profile: { id: "prf_mia", name: "Mia" } } (an id of letters, digits, _ and -). $me, a sight via the people or own, and a view's props.me then read Mia's person, a record of the people kind made on her first act, its id <account>~prf_mia, so it can only ever be that account's. It is a name, not a seat: the roles, grants and seatOf stay the account's, and everything the account could reach it still reaches. History says "Mia (Nick)". Without a profile, nothing changes. A person removed stays removed, and a kind that requires more than a name has none for a profile (people-unmakeable): $me then refuses, and the act is not offered. The seat headers carry it only where a host trusts them; otherwise it is the host's seatOf.

A person entered before they joined is bound, not made twice: bindPerson(store, joining, "person:bud", { author }) (@graview/core/binding); $me, own and self then read that record. Only the host's system seat binds; no grant may name bind-person (grant-gives-binding). A seat sees its own binding, the host all; others read its acts as the record's. A host reads through seenBy each time and keeps none of its methods, and forwards a change through answerSeenBy: re-read, never pass on, what a listener is handed.

What a view keeps for that viewer (graview.mine, graview-view-forms) goes where the host says, for the viewer it knows, handed to mount, <Embed> and PagesApp as viewerState:

const viewerState: ViewerState = {
  read: (view) => fetchMine(view),                    // that view's values, by its name
  write: (view, key, value) => keepMine(view, key, value), // { ok: true } or { ok: false, sentence }
};

Keep it out of the record store: it is never history, an export or an agent's.

What a view is told it cannot see

A worker view is handed props.unseen where its seat is kept from some of what it reads, so it says why a part is empty:

const { kinds = [], fields = {} } = graview.props.unseen ?? {};
if (kinds.includes("vendor")) draw("Sign in to see the vendors");

kinds: the kinds it reads the seat sees none of (no sight for its roles, an own or via sight and no id, a module off). fields: per kind it sees, the fields hide or fields may keep from it, read from the policy. Nothing of any record is said: no count, no id. A frame, somebody else's code, is handed kinds alone, of those its own reads names. Where everything a view reads is kept, the host draws its signIn line (or "Nothing here is shown to you.") instead of starting it. graview view check --seats runs a view as each seat the policy names and says what each could not see: for the owner's eyes only.

Then find out whether it worked

pnpm build && npx graview check ./dist/domain/app.js
  • sight-names-an-unknown-field (an error): a hide or fields naming a field none of the sight's kinds has — it keeps or shows nothing.
  • sight-hides-a-required-field (an error): a role may not see a field every record needs, so it is shown no such record at all.
  • sight-hides-a-labeled-field (a warning): a kind's label or description reads a field some role may not see; that role is told the name worked out without it.
  • act-reads-hidden-kind (a warning): an act a role may run reads a kind that role may not see — its condition, its refusal, a value it sets, or what a TypeScript act declares it reads. Let the role see the kind, keep the act from it, or read only what it sees.
  • rule-says-hidden-fact (a warning): a rule's message says a fact over a kind some role told of it may not see in full. A seat's problems are judged over what it sees.

And try it. The test that matters is a world with the hidden value and the same world with another, or none — everything the seat reads must be the same:

const told = (store: Store<AnySchema>) => JSON.stringify({
  snapshot: store.seenBy(parent).snapshot(),
  log: store.seenBy(parent).log.all(),
  problems: store.seenBy(parent).violations(),
  found: search(store, "555", { principal: parent }).hits,
});
expect(told(world("555 0101"))).toBe(told(world(undefined)));

Worked examples

  • packages/core/tests/unit/a-sight-can-hide-a-field.test.ts and packages/core/tests/support/hidden-phones.ts — the roster, world with and without a phone number, every read compared
  • packages/core/tests/unit/an-act-runs-under-its-callers-sight.test.ts — a hidden record, the same comparison for an act's answer
  • apps/gauntlet/src/domain/policy.ts — staff kept from the visitor, and where a speaker comes from kept from them too
  • packages/core/tests/document/fixtures/secret-santa.gdd.json — a sight via a relation

What the check cannot see

  • Whether a TypeScript act's describe reads a hidden field directly: the store withholds such an op from the seat as it runs, but the author is the one who knows it was meant.
  • Whether what a seat may not see is what the people involved expect. That is a conversation, not a declaration.