diff --git a/packages/core/src/admin/persona.ts b/packages/core/src/admin/persona.ts index 67aceeb..5e13763 100644 --- a/packages/core/src/admin/persona.ts +++ b/packages/core/src/admin/persona.ts @@ -209,7 +209,7 @@ export interface AttributePutParams extends PersonaHolderParams { type: string; valueType: AttributeValueType; /** - * The fact itself. Must agree with `valueType`; the agent refuses a document + * The value itself. Must agree with `valueType`; the agent refuses a document * where it does not. * * Typed `unknown` deliberately. The published schema places no type diff --git a/packages/core/src/persona/consent-view.ts b/packages/core/src/persona/consent-view.ts index c7b28f9..ea60f2b 100644 --- a/packages/core/src/persona/consent-view.ts +++ b/packages/core/src/persona/consent-view.ts @@ -111,7 +111,7 @@ function describePredicate(p: NonNullable): string { }[p.op] ?? p.op; const arg = typeof p.arg === "string" ? p.arg : JSON.stringify(p.arg); // The trailing clause is the point of the row, not a footnote: this rung is - // the only one where the verifier learns a fact without receiving a value. + // the only one where the verifier learns something without receiving a value. return `proves ${op} ${arg} — the value itself is not sent`; } diff --git a/packages/core/src/persona/contacts.ts b/packages/core/src/persona/contacts.ts index 8cd30e5..058265c 100644 --- a/packages/core/src/persona/contacts.ts +++ b/packages/core/src/persona/contacts.ts @@ -4,7 +4,7 @@ // identity out, these record what came back the other way. A contact is stored // **as received** and is never merged into the holder's own attributes — that // separation is the point. A contact is somebody else's account of themselves, -// not a fact the holder is asserting, and a store that blurred the two would +// not an attribute the holder is asserting, and a store that blurred the two would // let a peer's claim about their own name be re-presented as the holder's. // // Every contact is filed against `knownByPersona`: which of the holder's own diff --git a/packages/core/src/persona/step-up.ts b/packages/core/src/persona/step-up.ts index c436bc9..23dafdc 100644 --- a/packages/core/src/persona/step-up.ts +++ b/packages/core/src/persona/step-up.ts @@ -102,7 +102,7 @@ export interface DisclosureStepUpRequired { * Named for what it is. Nothing in it may be shown to a human or signed over * until {@link verifyDisclosureStepUp} has passed — the spec rule is that a * consumer verifies the proof *before* surfacing the reason, and here the - * reason includes the list of facts about to leave. + * reason includes the list of attributes about to leave. */ unverifiedApproveRequest: Record; } diff --git a/packages/core/tests/persona.step-up.mjs b/packages/core/tests/persona.step-up.mjs index 5250ba8..4e05ccf 100644 --- a/packages/core/tests/persona.step-up.mjs +++ b/packages/core/tests/persona.step-up.mjs @@ -39,11 +39,11 @@ async function approveRequest({ as = AGENT, previewId = PREVIEW, ctx = {} } = {} subject: "did:key:zHolder", sessionId: "sess-42", challenge: "a".repeat(32), - reason: "Approve disclosing 1 fact to did:key:zVerifier", + reason: "Approve disclosing 1 attribute to did:key:zVerifier", ext: { [AUTHZ_EXT]: { type: CONTEXT_TYPE, - summary: "Approve disclosing 1 fact to did:key:zVerifier", + summary: "Approve disclosing 1 attribute to did:key:zVerifier", risk: "high", action: { kind: "disclose", @@ -135,7 +135,7 @@ test("what the holder is shown comes out of the signature", async () => { assert.deepEqual(res.context.claimTypes, ["payment.card"]); assert.equal(res.context.verifierDid, "did:key:zVerifier"); assert.equal(res.context.purpose, "checkout"); - assert.equal(res.context.summary, "Approve disclosing 1 fact to did:key:zVerifier"); + assert.equal(res.context.summary, "Approve disclosing 1 attribute to did:key:zVerifier"); assert.equal(res.request.challenge, "a".repeat(32)); }); diff --git a/packages/extension/src/confirm.tsx b/packages/extension/src/confirm.tsx index fb1cfee..d3cb1e5 100644 --- a/packages/extension/src/confirm.tsx +++ b/packages/extension/src/confirm.tsx @@ -44,7 +44,7 @@ const isTaskConsent = params.get("kind") === "task"; // `kind=disclosure` selects the persona-disclosure surface. It is its own // surface rather than a variant of the login prompt because what it authorizes // is different in kind: the login prompt says "let this site sign you in", and -// this one says "let this verifier keep these facts about you". Sharing a +// this one says "let this verifier keep these attributes about you". Sharing a // component would mean one screen trying to word both. const isDisclosureConsent = params.get("kind") === "disclosure"; // The biometric-gated approver surface: Approve must run a fresh WebAuthn diff --git a/packages/extension/src/disclosure-consent.tsx b/packages/extension/src/disclosure-consent.tsx index 834d05e..221b3ac 100644 --- a/packages/extension/src/disclosure-consent.tsx +++ b/packages/extension/src/disclosure-consent.tsx @@ -16,7 +16,7 @@ // - **Withheld rows are drawn as warnings, not as claims.** They are the only // rows whose presence makes the disclosure smaller than it looks. // - **A predicate is drawn in the positive colour.** It is the strongest -// outcome on the screen — the verifier learns a fact and receives no value +// outcome on the screen — the verifier learns something and receives no value // — and drawing it in the same ink as an ordinary disclosure would hide the // one row where the holder gave away least. // - **Linkability leads when it is not `none`.** It sits above the claim list diff --git a/packages/extension/src/manager/claim-sensitivity.ts b/packages/extension/src/manager/claim-sensitivity.ts index d2655ca..ff756dd 100644 --- a/packages/extension/src/manager/claim-sensitivity.ts +++ b/packages/extension/src/manager/claim-sensitivity.ts @@ -1,4 +1,4 @@ -// How carefully a fact's value is shown to the person who owns it. +// How carefully an attribute's value is shown to the person who owns it. // // ## This is not a security control, and saying so is the point // @@ -133,7 +133,7 @@ const REGISTERED: Readonly> = { * §4's first rule is a per-attribute override the holder set explicitly, which * wins over the registry. No field carries one on the wire yet, so nothing here * can read it; when one exists it belongs *above* this call, not inside it, - * because "the holder decided" and "the registry says" are different facts and + * because "the holder decided" and "the registry says" are different attributes and * a UI that wants to explain the difference needs both. * * **The prefix walk is rule 3, and it only ever tightens.** An unregistered @@ -274,12 +274,12 @@ function emailLocal(text: string): string { } /** - * What to draw for a fact, and whether a *Show* control belongs beside it. + * What to draw for an attribute, and whether a *Show* control belongs beside it. * * `masked` is the caller's cue for two separate things and both matter: a * reveal control, and a rendering distinct from an absent value. A pane that * greys a mask the way it greys "not requested" has told the operator that a - * fact they hold is a fact they do not. + * attribute they hold is an attribute they do not. */ export function maskedFact(type: string, text: string): { text: string; masked: boolean } { const treatment = treatmentOf(type); diff --git a/packages/extension/src/manager/holder-gate.ts b/packages/extension/src/manager/holder-gate.ts index 45a19a3..6f97fab 100644 --- a/packages/extension/src/manager/holder-gate.ts +++ b/packages/extension/src/manager/holder-gate.ts @@ -60,7 +60,7 @@ export function holderGate(authority: Authority | null): string | null { if (!authority) return null; if (isUnscopedHolder(authority)) return null; return ( - "Your facts sit above every context, so reaching them takes authority of its own: " + + "Your attributes sit above every context, so reaching them takes authority of its own: " + "an agent credential with no context restriction, or one granted the " + "`persona-holder` capability. This console cannot see which you have — " + "`auth/whoami` reports roles and scopes, not capabilities — so it does not stop " + diff --git a/packages/extension/src/manager/identity-graph.ts b/packages/extension/src/manager/identity-graph.ts index f558474..797e4ec 100644 --- a/packages/extension/src/manager/identity-graph.ts +++ b/packages/extension/src/manager/identity-graph.ts @@ -1,21 +1,21 @@ -// The identity map's model: facts, faces, contexts, and what reaches what. +// The identity map's model: attributes, faces, contexts, and what reaches what. // -// The persona pane draws one picture of the whole model — facts on top, faces +// The persona pane draws one picture of the whole model — attributes on top, faces // in the middle, contexts below — and lights up everything a selection reaches. -// Which things light up is the substance of the picture: a fact that reaches +// Which things light up is the substance of the picture: an attribute that reaches // two contexts through one face is the holder's linkage made visible, and a // context that lights the wrong face is a lie about what it holds. So the graph // and the reach computation live here, out of the component, with no DOM and // no relative imports, where they can be tested the way `profile-entries.ts` // was. // -// Vocabulary on screen follows `design-docs/persona-vocabulary.md`: a fact, a +// Vocabulary on screen follows `design-docs/persona-vocabulary.md`: an attribute, a // face, a context, a persona that *wears* a face. The identifiers below keep the // spec's names where they name wire records (`profileId`, `personaDid`). import type { PoolAttribute, PoolProfile, PoolProfileEntry } from "@openvtc/pnm-core/admin"; -export interface FactNode { +export interface AttributeNode { id: string; type: string; label?: string | undefined; @@ -29,10 +29,10 @@ export interface FactNode { export interface FaceNode { id: string; name: string; - /** Facts this face selects by live reference. */ - factIds: string[]; + /** Attributes this face selects by live reference. */ + attributeIds: string[]; /** Entries that are not a live reference — pinned, overridden, inline. They - * still reach a context; they just do not draw to a fact card. */ + * still reach a context; they just do not draw to an attribute card. */ preserved: number; entries: PoolProfileEntry[]; version: number; @@ -65,14 +65,14 @@ export interface FaceLink { } export interface IdentityGraph { - facts: FactNode[]; + attributes: AttributeNode[]; faces: FaceNode[]; contexts: ContextNode[]; links: FaceLink[]; } export type Selection = - | { kind: "fact"; id: string } + | { kind: "attribute"; id: string } | { kind: "face"; id: string } | { kind: "context"; id: string } | { kind: "persona"; contextId: string; did: string }; @@ -111,7 +111,7 @@ export function buildGraph( profiles: readonly PoolProfile[], contexts: readonly ContextInput[], ): IdentityGraph { - const facts: FactNode[] = attributes.map((a) => ({ + const attributeNodes: AttributeNode[] = attributes.map((a) => ({ id: a.attributeId, type: a.type, label: a.label, @@ -127,7 +127,7 @@ export function buildGraph( return { id: p.profileId, name: p.name, - factIds: refs, + attributeIds: refs, preserved: p.entries.length - refs.length, entries: p.entries, version: p.version, @@ -166,28 +166,28 @@ export function buildGraph( .filter(([, list]) => list.length > 1) .map(([faceId, list]) => ({ faceId, wearers: list })); - return { facts, faces, contexts: contextNodes, links }; + return { attributes: attributeNodes, faces, contexts: contextNodes, links }; } /** Everything a selection reaches, in every direction it can reach. */ export interface Reach { - factIds: Set; + attributeIds: Set; faceIds: Set; personaKeys: Set; contextIds: Set; } function empty(): Reach { - return { factIds: new Set(), faceIds: new Set(), personaKeys: new Set(), contextIds: new Set() }; + return { attributeIds: new Set(), faceIds: new Set(), personaKeys: new Set(), contextIds: new Set() }; } /** * What lights up. * - * Reach runs **downwards from a fact** — the faces that select it, the personas + * Reach runs **downwards from an attribute** — the faces that select it, the personas * that wear those faces, the contexts they are in — and **upwards from a - * context** — its personas' faces and those faces' facts. A face reaches both - * ways. That asymmetry is the model: a fact's reach is where it *goes*; a + * context** — its personas' faces and those faces' attributes. A face reaches both + * ways. That asymmetry is the model: an attribute's reach is where it *goes*; a * context's reach is what it *holds*. * * Nothing here reads a context's copy. The picture is drawn from the pool and @@ -199,7 +199,7 @@ export function reachOf(graph: IdentityGraph, selection: Selection | null): Reac const out = empty(); if (!selection) return out; - const facesWithFact = (factId: string) => graph.faces.filter((f) => f.factIds.includes(factId)); + const facesWithAttribute = (attributeId: string) => graph.faces.filter((f) => f.attributeIds.includes(attributeId)); const wearersOf = (faceId: string) => graph.contexts.flatMap((c) => c.personas.filter((p) => p.faceId === faceId)); @@ -213,13 +213,13 @@ export function reachOf(graph: IdentityGraph, selection: Selection | null): Reac const lightFaceUp = (faceId: string) => { out.faceIds.add(faceId); const face = graph.faces.find((f) => f.id === faceId); - for (const id of face?.factIds ?? []) out.factIds.add(id); + for (const id of face?.attributeIds ?? []) out.attributeIds.add(id); }; switch (selection.kind) { - case "fact": - out.factIds.add(selection.id); - for (const face of facesWithFact(selection.id)) lightFaceDown(face.id); + case "attribute": + out.attributeIds.add(selection.id); + for (const face of facesWithAttribute(selection.id)) lightFaceDown(face.id); break; case "face": lightFaceUp(selection.id); @@ -246,13 +246,13 @@ export function reachOf(graph: IdentityGraph, selection: Selection | null): Reac return out; } -/** The faces a fact reaches, and the contexts beyond them — the sentence the +/** The faces an attribute reaches, and the contexts beyond them — the sentence the * detail strip says: "reaches 2 contexts through Glenn – Developer". */ -export function factReach( +export function attributeReach( graph: IdentityGraph, - factId: string, + attributeId: string, ): { faces: FaceNode[]; contextIds: string[]; wearers: PersonaNode[] } { - const faces = graph.faces.filter((f) => f.factIds.includes(factId)); + const faces = graph.faces.filter((f) => f.attributeIds.includes(attributeId)); const wearers = graph.contexts.flatMap((c) => c.personas.filter((p) => p.faceId !== null && faces.some((f) => f.id === p.faceId)), ); diff --git a/packages/extension/src/manager/panes/persona-editors.tsx b/packages/extension/src/manager/panes/persona-editors.tsx index 32524d3..f0e6994 100644 --- a/packages/extension/src/manager/panes/persona-editors.tsx +++ b/packages/extension/src/manager/panes/persona-editors.tsx @@ -6,9 +6,11 @@ // (`persona-setup.tsx`) compose the same forms rather than two drifting copies. // The pane (`persona.tsx`) decides which of the two to show. // -// Copy follows `design-docs/persona-vocabulary.md`: on screen it is a fact, a -// face, a context, and a persona that wears a face. The code keeps the spec's -// names (`attribute`, `profile`, `binding`) where they name wire records. +// Copy follows `design-docs/persona-vocabulary.md`: on screen it is an +// attribute, a face, a context, and a persona that wears a face. `attribute` +// is the one term the table does not translate — the screen says what the wire +// says — because the friendlier word it used to carry, `fact`, claimed a truth +// self-asserted values do not have. `profile` and `binding` still translate. import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { @@ -67,7 +69,7 @@ export function Label({ children }: { children: React.ReactNode }) { * `value` really is arbitrary JSON — a string attribute's value is a string — * so this must not assume an object, and must not print `[object Object]` for * the one case that is. `undefined` is the answer to a metadata-only listing - * and says so, rather than rendering as an empty cell that reads like a fact + * and says so, rather than rendering as an empty cell that reads like an attribute * with no value. * * **Not exported, and that is the enforcement.** Every value this pane draws @@ -86,7 +88,7 @@ function formatValue(value: unknown): { text: string; withheld: boolean } { } /** - * A fact's value, hidden if its type says it should be, with a *Show* beside + * An attribute's value, hidden if its type says it should be, with a *Show* beside * it when it is. * * Every place this pane draws a value goes through here, because "wherever it @@ -102,12 +104,12 @@ function formatValue(value: unknown): { text: string; withheld: boolean } { * let a UI string in this component imply otherwise. * * **Reveal is per value and lives in this component.** Not lifted to the pane - * keyed by fact id, which would be a store of "things the operator has + * keyed by attribute id, which would be a store of "things the operator has * unhidden" — one that survives selection changes, outlives the card the person * was looking at, and is one refactor away from a *Show all*. Local state * cannot become that: it dies with the element, so leaving the pane, reloading * the console or navigating anywhere re-hides everything, and revealing the - * same fact in two places is two deliberate acts rather than one. + * same attribute in two places is two deliberate acts rather than one. */ export function FactValue({ type, @@ -132,7 +134,7 @@ export function FactValue({ style={{ // A masked value is drawn at full strength; `c.faint` is this pane's // word for "the agent did not send one". Greying the mask too would - // make a fact the holder has look exactly like a fact they do not, + // make an attribute the holder has look exactly like an attribute they do not, // and the difference is the one thing a hidden value must still say. color: withheld ? c.faint : c.text, ...(masked && !shown ? { fontFamily: font.mono, letterSpacing: 0.5 } : {}), @@ -143,8 +145,8 @@ export function FactValue({ {masked && ( {onCancel && ( + label="Delete" preview={async () => { // Asked again rather than read off the map, so the answer is // current at the moment of the decision. const current = await personaProfileList(managerSender, parties); - return current.filter((p) => p.entries.some((e) => "ref" in e && e.ref === fact.id)); + return current.filter((p) => p.entries.some((e) => "ref" in e && e.ref === attribute.id)); }} renderPreview={(faces) => ( <> - Deleting a fact cannot be undone. - {fact.type} + Deleting an attribute cannot be undone. + {attribute.type} {faces.length === 0 ? ( No face shows it, so nothing stops showing anything. ) : ( <> {faces.length} face(s) show it and will stop: {faces.map((f) => f.name).join(", ")} - Every persona wearing one of those shows one fact fewer from the next hand-over onwards. Nothing already shared is affected — that has left. + Every persona wearing one of those shows one attribute fewer from the next hand-over onwards. Nothing already shared is affected — that has left. )} @@ -874,7 +874,7 @@ function DetailStrip({ needsForce={(faces) => faces.length > 0} forceLabel="Remove it from those faces too" commit={async (force) => { - await personaAttributeDelete(managerSender, { ...parties, attributeId: fact.id, cascade: force }); + await personaAttributeDelete(managerSender, { ...parties, attributeId: attribute.id, cascade: force }); }} onDone={onChanged} /> @@ -896,11 +896,11 @@ function DetailStrip({ Face {face.name} - shows {face.entries.length} fact{face.entries.length === 1 ? "" : "s"}{face.preserved > 0 ? ` (${face.preserved} pinned, shown differently, or only here)` : ""} + shows {face.entries.length} attribute{face.entries.length === 1 ? "" : "s"}{face.preserved > 0 ? ` (${face.preserved} pinned, shown differently, or only here)` : ""} {col("Worn by", wearers.length === 0 ? ( - Nobody yet. No context receives these facts. + Nobody yet. No context receives these attributes. ) : ( <> {wearers.map((w) => ( @@ -947,7 +947,7 @@ function DetailStrip({ ) : ( ctx.personas.filter((p) => p.faceId).map((p) => ( - a copy of {p.faceName ?? "a face"}, worn by {personaLabel(p.did)} · {p.claimCount} fact{p.claimCount === 1 ? "" : "s"} + a copy of {p.faceName ?? "a face"}, worn by {personaLabel(p.did)} · {p.claimCount} attribute{p.claimCount === 1 ? "" : "s"} )) ))} @@ -982,7 +982,7 @@ function DetailStrip({ {col("Wears", p.faceId ? ( <> - {p.faceName ?? "a face"} · {p.claimCount} fact{p.claimCount === 1 ? "" : "s"} copied into this context + {p.faceName ?? "a face"} · {p.claimCount} attribute{p.claimCount === 1 ? "" : "s"} copied into this context {link && ( {link.wearers.filter((w) => !(w.did === p.did && w.contextId === p.contextId)).map((w) => `${personaLabel(w.did)} in ${labelOf(w.contextId)}`).join(", ")} wear{link.wearers.length === 2 ? "s" : ""} the same face — same person to anyone who sees both. @@ -990,7 +990,7 @@ function DetailStrip({ )} ) : ( - Nothing. This persona is known here but shows no facts. + Nothing. This persona is known here but shows no attributes. ))} {col("Last left", lastLeft((d) => d.personaDid === p.did && d.contextId === ctx.id))} diff --git a/packages/extension/src/manager/panes/persona-setup.tsx b/packages/extension/src/manager/panes/persona-setup.tsx index 4fff199..7b8a683 100644 --- a/packages/extension/src/manager/panes/persona-setup.tsx +++ b/packages/extension/src/manager/panes/persona-setup.tsx @@ -1,10 +1,10 @@ -// The first five minutes: a fact, a face, a context. +// The first five minutes: an attribute, a face, a context. // // Shown while the holder has no face yet — the one state in which the identity // map has nothing to draw and a stack of empty bands would answer "what do I // do?" with silence. Three steps, in the order the model runs, each saying what // it changes before it is done; and beside the second, the exact card someone -// would receive, updating as facts are ticked, because that card is the whole +// would receive, updating as attributes are ticked, because that card is the whole // point of a face and the thing a person cannot picture from a list of // checkboxes. // @@ -26,7 +26,7 @@ type Step = 1 | 2 | 3; function Stepper({ step, reachable, onGo }: { step: Step; reachable: (s: Step) => boolean; onGo: (s: Step) => void }) { const items: [Step, string][] = [ - [1, "Add a fact or two"], + [1, "Add an attribute or two"], [2, "Make a face"], [3, "Be known somewhere"], ]; @@ -82,10 +82,10 @@ function Stepper({ step, reachable, onGo }: { step: Step; reachable: (s: Step) = /** The card a stranger would receive from a face — the thing a tick list * cannot convey. Rendered from the pool directly: at this step nothing has * been pushed anywhere yet, so the pool is the only source. */ -function StrangerCard({ facts, faceName }: { facts: PoolAttribute[]; faceName: string }) { - const name = facts.find((f) => f.type === "name" || f.type.startsWith("name.")); - const rest = facts.filter((f) => f !== name); - const provable = facts.some((f) => f.provenance.kind === "credentialBacked" && !f.stale); +function StrangerCard({ attributes, faceName }: { attributes: PoolAttribute[]; faceName: string }) { + const name = attributes.find((f) => f.type === "name" || f.type.startsWith("name.")); + const rest = attributes.filter((f) => f !== name); + const provable = attributes.some((f) => f.provenance.kind === "credentialBacked" && !f.stale); return (
@@ -93,7 +93,7 @@ function StrangerCard({ facts, faceName }: { facts: PoolAttribute[]; faceName: s Exactly this, and nothing else. It changes as you tick.
- {facts.length === 0 ? ( + {attributes.length === 0 ? ( Nothing ticked. A stranger would receive an empty card. ) : ( <> @@ -118,7 +118,7 @@ function StrangerCard({ facts, faceName }: { facts: PoolAttribute[]; faceName: s )}
- {facts.length > 0 && !provable && ( + {attributes.length > 0 && !provable && ( Nothing here is proven. These are things you said. A context can pass them on, but cannot show anyone they are true. @@ -174,7 +174,7 @@ export function GuidedSetup({ {denied && {denied}} reachableStep(s, { facts: attributes.length, faces: profiles.length })} + reachable={(s) => reachableStep(s, { attributes: attributes.length, faces: profiles.length })} onGo={setStep} /> @@ -188,7 +188,7 @@ export function GuidedSetup({ onDone={onChanged} /> {attributes.length > 0 && ( - +
{attributes.map((a) => (
@@ -205,13 +205,13 @@ export function GuidedSetup({

- A fact is one thing about you, held once — a name, a phone number, a handle. Nothing you add here - is visible anywhere yet. Faces choose among facts; a context receives a copy only when a persona + An attribute is one thing about you, held once — a name, a phone number, a handle. Nothing you add here + is visible anywhere yet. Faces choose among attributes; a context receives a copy only when a persona there wears one.

Two or three are plenty to start. You can always add more, and a face only ever shows what you - ticked — facts you add later stay out until you say otherwise. + ticked — attributes you add later stay out until you say otherwise.

@@ -234,9 +234,9 @@ export function GuidedSetup({ setStep(3); }} onCancel={() => setStep(1)} - cancelLabel="Back — add more facts" + cancelLabel="Back — add more attributes" /> - +
)} @@ -275,11 +275,11 @@ export function GuidedSetup({

- A persona is the identifier a context knows you by. Putting a face on it copies the face's facts + A persona is the identifier a context knows you by. Putting a face on it copies the face's attributes down into that context — and only that context. Nothing reads back up.

- Edit a fact later and every copy updates. Wear the same face in two contexts and anyone who sees + Edit an attribute later and every copy updates. Wear the same face in two contexts and anyone who sees you in both knows you are one person — the map will show that link the moment it exists.

diff --git a/packages/extension/src/manager/panes/persona.tsx b/packages/extension/src/manager/panes/persona.tsx index befdad6..d791c98 100644 --- a/packages/extension/src/manager/panes/persona.tsx +++ b/packages/extension/src/manager/panes/persona.tsx @@ -5,13 +5,13 @@ // // Everywhere else in the console, a context is the compartment: keys, DIDs, // memory and app-state all live inside one. The attribute pool and the profiles -// over it do not. There is one person here, with one set of facts about +// over it do not. There is one person here, with one set of attributes about // themselves, and the contexts are the places they choose to be known. // // So the pane is a picture rather than a filtered list — the identity map in -// `persona-map.tsx`: facts on top, faces in the middle, contexts below, with the +// `persona-map.tsx`: attributes on top, faces in the middle, contexts below, with the // one-way line drawn between. And for the holder who has nothing yet, the -// guided setup in `persona-setup.tsx`: a fact, a face, a context, in the order +// guided setup in `persona-setup.tsx`: an attribute, a face, a context, in the order // the model runs. This file loads what both need and decides which to show. // // ## The boundary runs through the middle of it @@ -40,7 +40,7 @@ // // ## Words // -// On screen: a fact, a face, a context, a persona that wears a face. See +// On screen: an attribute, a face, a context, a persona that wears a face. See // `design-docs/persona-vocabulary.md`. In code the spec's names stay where they // name wire records. @@ -180,7 +180,7 @@ export function PersonaPane({ if (attributes.error) { return (
- +
); } @@ -191,7 +191,7 @@ export function PersonaPane({ } if (!attributes.data || !profiles.data) return ; - // No face means nothing for the map to draw. A holder with facts and no face + // No face means nothing for the map to draw. A holder with attributes and no face // lands on step two; one with nothing on step one. if (showGuide) { return ( diff --git a/packages/extension/src/manager/persona-flow.ts b/packages/extension/src/manager/persona-flow.ts index ec9715f..8c1e5f2 100644 --- a/packages/extension/src/manager/persona-flow.ts +++ b/packages/extension/src/manager/persona-flow.ts @@ -31,18 +31,18 @@ export function showsGuide(state: { faces: number | null; guiding: boolean; skip /** * Whether a holder can jump to a step of the guided setup. * - * Step one is always reachable — going back to add another fact is the most + * Step one is always reachable — going back to add another attribute is the most * ordinary thing a person wants here, and until the stepper answered clicks * the only route was a button labelled "Cancel", which reads as abandoning the * whole flow rather than stepping back one. * * The later two are reachable only once they have something to work on: a face - * cannot be composed out of no facts, and a persona cannot wear a face that + * cannot be composed out of no attributes, and a persona cannot wear a face that * does not exist. Reaching them empty would present a form whose every control * refuses, which is a worse answer than not offering the step. */ -export function reachableStep(step: 1 | 2 | 3, have: { facts: number; faces: number }): boolean { +export function reachableStep(step: 1 | 2 | 3, have: { attributes: number; faces: number }): boolean { if (step === 1) return true; - if (step === 2) return have.facts > 0; + if (step === 2) return have.attributes > 0; return have.faces > 0; } diff --git a/packages/extension/src/manager/profile-entries.ts b/packages/extension/src/manager/profile-entries.ts index 80c4348..dcaf0ee 100644 --- a/packages/extension/src/manager/profile-entries.ts +++ b/packages/extension/src/manager/profile-entries.ts @@ -19,7 +19,7 @@ // - **Seed the ticks from every `ref`** — pinned and overridden entries have // one too — and saving writes the same attribute twice: once live, from the // tick, and once pinned, from the entry that was carried through. One -// apparently unchanged edit, two entries for one fact. +// apparently unchanged edit, two entries for one attribute. // // So the split is by entry *form*, not by whether an entry has a `ref`. diff --git a/packages/extension/tests/manager-claim-sensitivity.test.mts b/packages/extension/tests/manager-claim-sensitivity.test.mts index c297068..75ccc30 100644 --- a/packages/extension/tests/manager-claim-sensitivity.test.mts +++ b/packages/extension/tests/manager-claim-sensitivity.test.mts @@ -122,8 +122,8 @@ test("emailLocal keeps the domain and nothing else, and refuses to guess", () => test("a hidden value is not an empty one", () => { // A mask that rendered as nothing — or as the pane's word for a value the - // agent did not send — would tell the operator that a fact they hold is a - // fact they do not. `masked` is what the pane draws differently on; it must + // agent did not send — would tell the operator that an attribute they hold is a + // attribute they do not. `masked` is what the pane draws differently on; it must // be set, and the text must not be blank. const hidden = maskedFact("gov.id.passport", "X1234567"); assert.equal(hidden.masked, true); diff --git a/packages/extension/tests/manager-holder-gate.test.mts b/packages/extension/tests/manager-holder-gate.test.mts index 7cbde4e..481805d 100644 --- a/packages/extension/tests/manager-holder-gate.test.mts +++ b/packages/extension/tests/manager-holder-gate.test.mts @@ -36,7 +36,11 @@ test("a context-scoped admin is cautioned, not refused", () => { test("the caution speaks the agreed vocabulary", () => { const note = holderGate(authority(["application"], ["work"]))!; - for (const banned of ["attribute", "profile", "binding", "disclosure", "provenance"]) { + // `attribute` came *off* this list: the table now uses the spec word on + // screen too, because the word it used to translate to — `fact` — asserted + // a truth the model cannot promise and already meant a verified policy input + // in `vtc-service`. See "Why not 'fact'" in the vocabulary guide. + for (const banned of ["fact", "profile", "binding", "disclosure", "provenance"]) { assert.ok( !note.toLowerCase().includes(banned), `"${banned}" is kept off the screen (design-docs/persona-vocabulary.md): ${note}`, diff --git a/packages/extension/tests/manager-identity-graph.test.mts b/packages/extension/tests/manager-identity-graph.test.mts index 4cc248a..4b36a0c 100644 --- a/packages/extension/tests/manager-identity-graph.test.mts +++ b/packages/extension/tests/manager-identity-graph.test.mts @@ -1,8 +1,8 @@ // The identity map's reach — what lights up, and in which direction. // -// A fact's reach is where it goes; a context's reach is what it holds. Get the -// direction wrong and the picture claims a context holds a fact it was never -// given, or that a fact reaches a context it does not — the second being the +// An attribute's reach is where it goes; a context's reach is what it holds. Get the +// direction wrong and the picture claims a context holds an attribute it was never +// given, or that an attribute reaches a context it does not — the second being the // holder concluding no linkage exists when one does. Every case here has a // paired positive, because a reach function that lights nothing satisfies every // "does not light" assertion there is. @@ -11,7 +11,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildGraph, - factReach, + attributeReach, personaKey, reachOf, type ContextInput, @@ -54,10 +54,10 @@ const CTXS = [ ]; const G = buildGraph(ATTRS, FACES, CTXS); -test("a face's fact ids are its live references only", () => { +test("a face's attribute ids are its live references only", () => { // A pinned entry still reaches a context, but it does not draw to the live - // card — that card would then read as "changes when the fact does". - assert.deepEqual(G.faces.find((f) => f.id === "F-pub")?.factIds, ["f-name"]); + // card — that card would then read as "changes when the attribute does". + assert.deepEqual(G.faces.find((f) => f.id === "F-pub")?.attributeIds, ["f-name"]); assert.equal(G.faces.find((f) => f.id === "F-pub")?.preserved, 1); }); @@ -69,8 +69,8 @@ test("a face worn by two personas is a link; a face worn by one is not", () => { ); }); -test("a fact reaches down: its faces, their wearers, their contexts", () => { - const r = reachOf(G, { kind: "fact", id: "f-phone" }); +test("an attribute reaches down: its faces, their wearers, their contexts", () => { + const r = reachOf(G, { kind: "attribute", id: "f-phone" }); assert.deepEqual([...r.faceIds], ["F-dev"]); assert.deepEqual([...r.contextIds].sort(), ["openvtc", "vta"]); assert.ok(r.personaKeys.has(personaKey("openvtc", "did:a"))); @@ -79,33 +79,33 @@ test("a fact reaches down: its faces, their wearers, their contexts", () => { assert.ok(!r.personaKeys.has(personaKey("vta", "did:c"))); }); -test("a fact in two faces reaches through both", () => { - const r = reachOf(G, { kind: "fact", id: "f-name" }); +test("an attribute in two faces reaches through both", () => { + const r = reachOf(G, { kind: "attribute", id: "f-name" }); assert.deepEqual([...r.faceIds].sort(), ["F-dev", "F-pub"]); }); -test("a fact only pinned, in a face nobody wears, reaches nowhere", () => { - const r = reachOf(G, { kind: "fact", id: "f-signal" }); +test("an attribute only pinned, in a face nobody wears, reaches nowhere", () => { + const r = reachOf(G, { kind: "attribute", id: "f-signal" }); assert.equal(r.faceIds.size, 0); assert.equal(r.contextIds.size, 0); }); -test("a context reaches up: its personas' faces and those faces' facts — not every fact", () => { +test("a context reaches up: its personas' faces and those faces' attributes — not every attribute", () => { const r = reachOf(G, { kind: "context", id: "openvtc" }); assert.deepEqual([...r.faceIds], ["F-dev"]); - assert.deepEqual([...r.factIds].sort(), ["f-name", "f-phone"]); - assert.ok(!r.factIds.has("f-signal"), "a context must not light a fact it was never given"); + assert.deepEqual([...r.attributeIds].sort(), ["f-name", "f-phone"]); + assert.ok(!r.attributeIds.has("f-signal"), "a context must not light an attribute it was never given"); }); test("a context where nobody is known lights only itself", () => { const r = reachOf(G, { kind: "context", id: "webvh" }); assert.deepEqual([...r.contextIds], ["webvh"]); - assert.equal(r.faceIds.size + r.factIds.size + r.personaKeys.size, 0); + assert.equal(r.faceIds.size + r.attributeIds.size + r.personaKeys.size, 0); }); test("a face reaches both ways", () => { const r = reachOf(G, { kind: "face", id: "F-dev" }); - assert.deepEqual([...r.factIds].sort(), ["f-name", "f-phone"]); + assert.deepEqual([...r.attributeIds].sort(), ["f-name", "f-phone"]); assert.deepEqual([...r.contextIds].sort(), ["openvtc", "vta"]); }); @@ -121,7 +121,7 @@ test("a persona lights its own context and face, not its neighbours'", () => { test("no selection lights nothing", () => { const r = reachOf(G, null); - assert.equal(r.factIds.size + r.faceIds.size + r.contextIds.size + r.personaKeys.size, 0); + assert.equal(r.attributeIds.size + r.faceIds.size + r.contextIds.size + r.personaKeys.size, 0); }); test("an unreadable context is carried as unreadable, not as empty", () => { @@ -132,8 +132,8 @@ test("an unreadable context is carried as unreadable, not as empty", () => { assert.equal(g.contexts[0]?.personas.length, 0); }); -test("factReach says where a fact goes in the words the strip uses", () => { - const r = factReach(G, "f-phone"); +test("attributeReach says where an attribute goes in the words the strip uses", () => { + const r = attributeReach(G, "f-phone"); assert.deepEqual(r.faces.map((f) => f.name), ["Developer"]); assert.deepEqual(r.contextIds.sort(), ["openvtc", "vta"]); assert.equal(r.wearers.length, 2); diff --git a/packages/extension/tests/manager-persona-guide.test.mts b/packages/extension/tests/manager-persona-guide.test.mts index 12c5d61..d2081ec 100644 --- a/packages/extension/tests/manager-persona-guide.test.mts +++ b/packages/extension/tests/manager-persona-guide.test.mts @@ -69,16 +69,16 @@ test("skipping is sticky, even when the last face goes", () => { import { reachableStep } from "../src/manager/persona-flow.ts"; -const reach = (step: 1 | 2 | 3, facts: number, faces: number) => reachableStep(step, { facts, faces }); +const reach = (step: 1 | 2 | 3, attributes: number, faces: number) => reachableStep(step, { attributes, faces }); test("step one is always reachable, including from a standing start", () => { assert.equal(reach(1, 0, 0), true); assert.equal(reach(1, 3, 1), true); }); -test("a face cannot be composed out of no facts", () => { +test("a face cannot be composed out of no attributes", () => { assert.equal(reach(2, 0, 0), false); - // The pair: one fact is enough to have something to tick. + // The pair: one attribute is enough to have something to tick. assert.equal(reach(2, 1, 0), true); }); @@ -90,5 +90,5 @@ test("a persona cannot wear a face that does not exist", () => { test("an unreachable step is refused rather than shown empty", () => { // Reaching a step with nothing to work on presents a form whose every // control refuses — a worse answer than not offering the step at all. - assert.equal(reach(2, 0, 1), false, "facts, not faces, are what step two needs"); + assert.equal(reach(2, 0, 1), false, "attributes, not faces, are what step two needs"); }); diff --git a/packages/extension/tests/manager-profile-entries.test.mts b/packages/extension/tests/manager-profile-entries.test.mts index f77186a..563108d 100644 --- a/packages/extension/tests/manager-profile-entries.test.mts +++ b/packages/extension/tests/manager-profile-entries.test.mts @@ -74,7 +74,7 @@ test("an unchanged edit round-trips the profile exactly", () => { test("no attribute is ever named twice", () => { // The duplicate bug: a pinned entry has a `ref`, so seeding the ticks from // every ref ticks its attribute AND carries the pinned entry through. The - // profile then presents one fact through two entries, from an edit in which + // profile then presents one attribute through two entries, from an edit in which // the operator touched nothing. const ticked = ["a-live", "a-pinned", "a-overridden"]; const out = composeEntries(ALL, ticked); diff --git a/packages/extension/tests/persona-pane.render.test.mts b/packages/extension/tests/persona-pane.render.test.mts index e47358e..d87960a 100644 --- a/packages/extension/tests/persona-pane.render.test.mts +++ b/packages/extension/tests/persona-pane.render.test.mts @@ -21,7 +21,7 @@ import { buildGraph } from "../src/manager/identity-graph.js"; const HOLDER = { session: { id: "s" }, roles: ["admin"], scopes: [] }; -const fact = (id: string, type: string, value: string) => ({ +const attribute = (id: string, type: string, value: string) => ({ attributeId: id, type, valueType: "string" as const, @@ -50,7 +50,7 @@ const context = (id: string, name: string) => ({ // The fixture is a registered token because these tests are about something // else; the masking of an unregistered one is asserted deliberately further // down. -const FACTS = [fact("f1", "name.legal", "Glenn Gore"), fact("f2", "phone.mobile", "+65 8262 2325")]; +const FACTS = [attribute("f1", "name.legal", "Glenn Gore"), attribute("f2", "phone.mobile", "+65 8262 2325")]; const CONTEXTS = [context("openvtc", "OpenVTC"), context("vta", "Verifiable Trust Agent")]; // ── The blank pane (#179) ─────────────────────────────────────────────────── @@ -77,12 +77,12 @@ test("making a face does not loop the renderer", async () => { { chrome: { runtime: { sendMessage: a.sendMessage } } }, ); - assert.match(ui.text(), /Make a face/, "the guide should be on step two with facts and no face"); + assert.match(ui.text(), /Make a face/, "the guide should be on step two with attributes and no face"); assert.match(ui.text(), /What a stranger would receive/); // Ticking is what drove the loop: the scrape ran, set state, and re-rendered. const boxes = ui.all('input[type="checkbox"]'); - assert.ok(boxes.length >= 2, `expected a tick per fact, saw ${boxes.length}`); + assert.ok(boxes.length >= 2, `expected a tick per attribute, saw ${boxes.length}`); await ui.check(boxes[0]!); await ui.check(boxes[1]!); @@ -96,7 +96,7 @@ test("making a face does not loop the renderer", async () => { test("the stranger card starts empty and says so", async () => { // The paired negative: an empty card is a real state with its own sentence, // not a blank area. Without this, the assertion above is satisfied by a card - // that shows every fact regardless of the ticks. + // that shows every attribute regardless of the ticks. const a = agent({}); const ui = await render( h(GuidedSetup, { @@ -112,7 +112,7 @@ test("the stranger card starts empty and says so", async () => { { chrome: { runtime: { sendMessage: a.sendMessage } } }, ); assert.match(ui.text(), /Nothing ticked/); - assert.doesNotMatch(ui.text(), /Glenn Gore/, "an unticked fact must not appear on the card"); + assert.doesNotMatch(ui.text(), /Glenn Gore/, "an unticked attribute must not appear on the card"); await ui.unmount(); }); @@ -137,7 +137,7 @@ test("a completed step in the stepper is a way back to it", async () => { ); assert.match(ui.text(), /Make a face/); - const backToOne = ui.byText('[role="button"]', "Add a fact or two"); + const backToOne = ui.byText('[role="button"]', "Add an attribute or two"); assert.ok(backToOne, "the completed first step should be pressable"); await ui.click(backToOne!); assert.match(ui.text(), /Why start here/, "clicking step one should return to it"); @@ -330,7 +330,7 @@ test("a context the agent would not answer for is not folded away as empty", asy // ── Values a shoulder should not collect (#185) ───────────────────────────── // -// The console draws the holder's own facts, so a passport number sits on screen +// The console draws the holder's own attributes, so a passport number sits on screen // for as long as the pane is open — through a screen share, a screenshot, and // anyone walking past. Hiding it is worth doing and is worth being precise // about what it is: the value was fetched before any of this ran, so this @@ -343,10 +343,10 @@ test("a context the agent would not answer for is not folded away as empty", asy // only a render sees it. const SECRETS = [ - fact("f1", "name.legal", "Glenn Gore"), - fact("f2", "phone.mobile", "+65 8262 2325"), - fact("f3", "gov.id.passport", "X1234567"), - fact("f4", "x:acme.badge", "BADGE-99"), + attribute("f1", "name.legal", "Glenn Gore"), + attribute("f2", "phone.mobile", "+65 8262 2325"), + attribute("f3", "gov.id.passport", "X1234567"), + attribute("f4", "x:acme.badge", "BADGE-99"), ]; /** The map, mounted over `SECRETS` with nothing selected. */ @@ -385,7 +385,7 @@ test("a sensitive value is not on the map until it is asked for", async () => { // A hidden value is drawn, not omitted. Rendering nothing — or rendering the // pane's phrase for a value the agent did not send — would say the holder - // does not have a fact they do have. + // does not have an attribute they do have. assert.match(screen, /••••/, "a hidden value still occupies its row"); assert.doesNotMatch(screen, /not requested/, "hidden is not the same state as absent"); assert.match(screen, /•••• 25/, "the tail the holder recognises their own number by survives"); @@ -396,7 +396,7 @@ test("a sensitive value is not on the map until it is asked for", async () => { test("Show reveals one value, and only the one that was pressed", async () => { const ui = await mapOverSecrets(); const controls = shows(ui); - assert.equal(controls.length, 3, "one control per hidden fact, and never a single global one"); + assert.equal(controls.length, 3, "one control per hidden attribute, and never a single global one"); await ui.click(controls[0]!); const screen = ui.text(); @@ -404,11 +404,11 @@ test("Show reveals one value, and only the one that was pressed", async () => { assert.doesNotMatch(screen, /X1234567/, "and reveals nothing else"); assert.doesNotMatch(screen, /BADGE-99/); - // The card underneath is a click target — it selects the fact and opens the + // The card underneath is a click target — it selects the attribute and opens the // strip below the map. Revealing a value must not do that too: the operator // pressed Show, and the screen they were reading changing under them is the // symptom of a missing `stopPropagation`. - assert.doesNotMatch(screen, /Last left/, "revealing a value must not also select the fact"); + assert.doesNotMatch(screen, /Last left/, "revealing a value must not also select the attribute"); await ui.unmount(); });