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
64 changes: 64 additions & 0 deletions packages/extension/src/manager/correlation-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,67 @@ export function crossingWords(row: RankedFinding): string | null {
return null;
}
}

// ## A finding with no attribute is about faces
//
// The agent reports a value that only faces hold — typed into a face as an
// inline entry, or shown in place of an attribute by an override — with no
// `attributeId`, because there is no attribute. The map places findings on
// attribute cards, so without this such a finding was counted in the summary
// and drawn nowhere: "2 links" above one visible one, which is the numbers not
// closing, the thing `tallyContexts` exists to prevent one band up.

/** One face a face-only finding names. */
export interface NamedFace {
profileId: string;
/** The holder's own name for the face, where the console holds the record. */
name: string | null;
/** The context a context-local face lives in. The console does not list
* those faces, so the context is how the holder finds it. */
contextId: string | null;
}

/** Findings the map cannot place on an attribute card. */
export function faceOnly(ranked: readonly RankedFinding[]): RankedFinding[] {
return ranked.filter((r) => !r.finding.attributeId);
}

/**
* The faces one finding names, each once, in the agent's order.
*
* A face appears in `sharedWith` once per context it is worn in, and the
* holder is being told *which faces*, so repeats collapse. A profile the
* console holds is named; one it does not is a context-local face, and its
* context is the only handle there is.
*/
export function facesNamed(
finding: CorrelationFinding,
profiles: readonly { profileId: string; name: string }[],
): NamedFace[] {
const out: NamedFace[] = [];
const seen = new Set<string>();
for (const s of (finding.sharedWith ?? []) as { profileId?: string; contextId?: string }[]) {
if (!s.profileId || seen.has(s.profileId)) continue;
seen.add(s.profileId);
const held = profiles.find((p) => p.profileId === s.profileId);
out.push({
profileId: s.profileId,
name: held?.name ?? null,
contextId: held ? null : (s.contextId ?? null),
});
}
return out;
}

/** "Market and a face kept only in ctx-b" — names, never ids, where there are names. */
export function facesWords(faces: readonly NamedFace[]): string {
const words = faces.map((f) =>
f.name !== null
? f.name
: f.contextId !== null
? `a face kept only in ${f.contextId}`
: "a face this console cannot name",
);
if (words.length <= 1) return words[0] ?? "No face";
return `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}`;
}
24 changes: 22 additions & 2 deletions packages/extension/src/manager/panes/persona-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,15 @@ import type { PoolFacet } from "@openvtc/pnm-core/admin";
import { familyOf, familyStyle, FAMILY_ORDER, type Family } from "../attribute-family.js";
import { worldHue } from "../world-colour.js";
import { worldsOfAttribute, worldOfFace, movePlan } from "../world-model.js";
import { rankFindings, tallyCrossings, crossingWords, type RankedFinding } from "../correlation-model.js";
import {
rankFindings,
tallyCrossings,
crossingWords,
faceOnly,
facesNamed,
facesWords,
type RankedFinding,
} from "../correlation-model.js";
import { provenanceWords, labelSaysSomethingElse, staleWords } from "../attribute-words.js";
import { unappliedClaimTypes } from "@openvtc/pnm-core/persona";
import {
Expand Down Expand Up @@ -900,7 +908,7 @@ export function IdentityMap({
</div>
{checking && checking !== "Asking your agent…" && <Note tone="danger">{checking}</Note>}
{findings && findings.length === 0 && (
<Note tone="accent">Your agent finds no two attributes holding the same value. That is an answer, not an empty result.</Note>
<Note tone="accent">Your agent finds no value shown in two places — not by two attributes, and not typed into two faces. That is an answer, not an empty result.</Note>
)}
{findings && findings.length > 0 && (() => {
// Counted from one place, so the numbers cannot disagree with the rows
Expand Down Expand Up @@ -933,6 +941,18 @@ export function IdentityMap({
a world. Making some worlds is what answers that.
</span>
)}
{/* A value only faces hold has no attribute card to sit on, so it
is listed here or it is counted above and shown nowhere. */}
{faceOnly(ranked).map((r, i) => {
const words = crossingWords(r);
return (
<span key={`face-link-${i}`}>
<strong>{facesWords(facesNamed(r.finding, profiles))}</strong> show the same
value, typed into each rather than drawn from your attributes.{" "}
{words ?? "Anyone who sees both knows they are the same person."}
</span>
);
})}
</div>
</Note>
);
Expand Down
25 changes: 25 additions & 0 deletions packages/extension/tests/manager-correlation-model.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,28 @@ test("severity is never touched by any of this", () => {
assert.equal(ranked.find((r) => r.crossing === "within")!.finding.severity, "high");
assert.equal(ranked.find((r) => r.crossing === "crosses")!.finding.severity, "low");
});

test("a finding with no attribute is kept out of the card map and names its faces", async () => {
const { faceOnly, facesNamed, facesWords } = await import("../src/manager/correlation-model.ts");
const onCard = finding({ attributeId: "01A", why: "card" });
const faces = finding({
why: "faces",
sharedWith: [
{ profileId: "01F", contextId: "ctx-a", personaDid: "did:x" },
{ profileId: "01F", contextId: "ctx-c", personaDid: "did:y" },
{ profileId: "01L", contextId: "ctx-b" },
],
});
const ranked = rankFindings([onCard, faces], WORLDS);
assert.deepEqual(faceOnly(ranked).map((r) => r.finding.why), ["faces"]);

const named = facesNamed(faces, [{ profileId: "01F", name: "Market" }]);
assert.deepEqual(named, [
// Worn in two contexts, named once: the holder is being told which faces.
{ profileId: "01F", name: "Market", contextId: null },
// Not a face the console lists — a context-local one — so its context is
// the handle.
{ profileId: "01L", name: null, contextId: "ctx-b" },
]);
assert.equal(facesWords(named), "Market and a face kept only in ctx-b");
});
Loading