diff --git a/packages/extension/src/manager/panes/persona-map.tsx b/packages/extension/src/manager/panes/persona-map.tsx
index 898d450..958248b 100644
--- a/packages/extension/src/manager/panes/persona-map.tsx
+++ b/packages/extension/src/manager/panes/persona-map.tsx
@@ -121,6 +121,27 @@ function standingWords(tally: ContextTally): string {
return parts.join(" · ");
}
+/**
+ * Whether the holder's own label is telling the reader anything the value does
+ * not already say.
+ *
+ * A label is a note to self — "work mobile", "the flat" — and it earns its
+ * place beside the value. When it *is* the value it earns nothing: a `company`
+ * attribute labelled "Affinidi" holding "Affinidi" drew **Affinidi · Affinidi**,
+ * which reads as a stutter and, worse, as two facts.
+ *
+ * Compared case- and space-insensitively, because "affinidi" beside "Affinidi"
+ * is the same stutter with a different shift key. Only a string value is
+ * compared: a JSON object rendered beside a label never repeats it, and
+ * stringifying one here to find out would be work in aid of a case that cannot
+ * arise.
+ */
+function labelSaysSomethingElse(label: string | undefined, value: unknown): boolean {
+ if (!label) return false;
+ if (typeof value !== "string") return true;
+ return label.trim().toLowerCase() !== value.trim().toLowerCase();
+}
+
function staleWords(reason: string | undefined): string {
switch (reason) {
case "expired":
@@ -641,7 +662,7 @@ export function IdentityMap({
on a card holding nothing the sentence that
matters is the one about where the value is. It
returns the moment the value does. */}
- {f.label && f.value !== undefined && (
+ {f.value !== undefined && labelSaysSomethingElse(f.label, f.value) && (
{f.label} ·
)}
{
+ // `sensitivity: "normal"` because `company` is unregistered and would
+ // otherwise be masked — which is how the pane the report came from was set
+ // up, and without it this test would pass on a card showing no value at all.
+ const doubled = [{ ...attribute("f1", "company", "Affinidi"), label: "Affinidi", sensitivity: "normal" }];
+ const a = agent({});
+ const ui = await render(
+ h(IdentityMap, {
+ parties: PARTIES,
+ authority: HOLDER,
+ registry: REGISTRY,
+ graph: buildGraph(doubled, [], []),
+ attributes: doubled,
+ profiles: [],
+ records: CONTEXTS,
+ history: [],
+ onReveal: async () => "never asked",
+ onChanged: () => {},
+ }),
+ { chrome: { runtime: { sendMessage: a.sendMessage } } },
+ );
+ const text = ui.text();
+ assert.match(text, /Affinidi/, "the value is still there");
+ assert.doesNotMatch(text, /Affinidi · Affinidi/);
+ await ui.unmount();
+});
+
+test("a label that says something the value does not is kept", async () => {
+ // The paired positive, and the reason the check is not simply "hide labels":
+ // "work mobile" beside a number is the whole point of having one.
+ const noted = [{ ...attribute("f2", "phone.mobile", "+65 8262 2325"), label: "work mobile" }];
+ const a = agent({});
+ const ui = await render(
+ h(IdentityMap, {
+ parties: PARTIES,
+ authority: HOLDER,
+ registry: REGISTRY,
+ graph: buildGraph(noted, [], []),
+ attributes: noted,
+ profiles: [],
+ records: CONTEXTS,
+ history: [],
+ onReveal: async () => "never asked",
+ onChanged: () => {},
+ }),
+ { chrome: { runtime: { sendMessage: a.sendMessage } } },
+ );
+ assert.match(ui.text(), /work mobile ·/);
+ await ui.unmount();
+});
+
+test("the same word in a different case is the same stutter", async () => {
+ const cased = [{ ...attribute("f3", "company", "Affinidi"), label: "affinidi ", sensitivity: "normal" }];
+ const a = agent({});
+ const ui = await render(
+ h(IdentityMap, {
+ parties: PARTIES,
+ authority: HOLDER,
+ registry: REGISTRY,
+ graph: buildGraph(cased, [], []),
+ attributes: cased,
+ profiles: [],
+ records: CONTEXTS,
+ history: [],
+ onReveal: async () => "never asked",
+ onChanged: () => {},
+ }),
+ { chrome: { runtime: { sendMessage: a.sendMessage } } },
+ );
+ assert.doesNotMatch(ui.text(), /affinidi ·/i);
+ await ui.unmount();
+});