Skip to content

Commit 5fdb53c

Browse files
authored
Merge pull request #46 from AdaWorldAPI/claude/osint-soa-rebake
osint: per-node facet tenant (dynamic categories) + deterministic bake
2 parents 2308e10 + 079320a commit 5fdb53c

3 files changed

Lines changed: 248 additions & 21 deletions

File tree

cockpit/src/OsintGraph.tsx

Lines changed: 157 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ const REL_COLOR = [
5959
// rel codes that make up the dimension layer: VALID_FOR (8) + the facets (10..15).
6060
const isFacetRel = (r: number) => r === 8 || (r >= 10 && r <= 15);
6161

62+
// The 6 dual-use facet AXES in tenant-byte order (value[1..=6]). The SoA tenant
63+
// tail ships one code per axis per node; the facet lens groups nodes by them
64+
// LIVE (the dynamic/residual layer — the twin of the materialized facet edges).
65+
const FACET_AXES_UI = ['militaryUse', 'civicUse', 'airo:type', 'MLType', 'purpose', 'capacity'];
66+
// categorical palette for facet codes (code 0 = absent → dim slate).
67+
const FACET_PALETTE = [
68+
'#4dd0e1', '#ffb547', '#35d07f', '#9b8cff', '#ff637d', '#c792ea',
69+
'#7fd1c7', '#f0a868', '#6cf0ff', '#b5e853', '#ff8fab', '#8fb8ff',
70+
];
71+
const facetColor = (code: number) =>
72+
code === 0 ? '#2a3a4a' : FACET_PALETTE[(code - 1) % FACET_PALETTE.length];
73+
6274
const DIM_NODE = { background: 'rgba(10,14,23,0.55)', border: '#26323f' };
6375
const DIM_EDGE = 'rgba(50,66,84,0.12)';
6476
const ACTIVE = '#6cf0ff';
@@ -69,6 +81,9 @@ interface Soa {
6981
cls: Uint8Array;
7082
edges: Array<{ s: number; t: number; r: number }>;
7183
labels: string[];
84+
// per-node facet tenant: 6 codes (value[1..=6]) × nodeCount, or null if the
85+
// asset predates the tenant tail. The dynamic attribute the facet lens groups by.
86+
tenants: Uint8Array | null;
7287
}
7388

7489
/** One readable step of the reasoning traversal, streamed into the readout. */
@@ -89,6 +104,7 @@ interface GraphApi {
89104
fireLens: (angleIdx: number) => void;
90105
clear: () => void;
91106
setDims: (show: boolean) => void;
107+
setFacet: (axis: number | null) => void;
92108
}
93109

94110
// Decode the OSO1 wire: magic(4) | nodeCount u32 | edgeCount u32 |
@@ -127,7 +143,14 @@ function decodeSoa(buf: ArrayBuffer): Soa {
127143
off += len;
128144
}
129145
}
130-
return { nodeCount, edgeCount, cls, edges, labels };
146+
// optional tenant tail (OSO1 additive): node_count × 6 facet bytes (value[1..=6]).
147+
// Old assets stop after the labels; new ones carry the per-node attribute here.
148+
let tenants: Uint8Array | null = null;
149+
if (off + nodeCount * 6 <= dv.byteLength) {
150+
tenants = new Uint8Array(buf, off, nodeCount * 6);
151+
off += nodeCount * 6;
152+
}
153+
return { nodeCount, edgeCount, cls, edges, labels, tenants };
131154
}
132155

133156
// vis-network options tuned to the Palantir look: hollow ring nodes (dark fill
@@ -266,13 +289,16 @@ export function OsintGraph() {
266289
const hostRef = useRef<HTMLDivElement>(null);
267290
const netRef = useRef<Network | null>(null);
268291
const apiRef = useRef<GraphApi | null>(null);
292+
const facetAxisRef = useRef<number | null>(null); // mirrors facetAxis for the build closures
269293
const [soa, setSoa] = useState<Soa | null>(null);
270294
const [error, setError] = useState<string | null>(null);
271295
const [status, setStatus] = useState('loading SoA…');
272296
const [readout, setReadout] = useState<Readout | null>(null);
273297
const [search, setSearch] = useState('');
274298
const [angle, setAngle] = useState<number | null>(null);
275299
const [showDims, setShowDims] = useState(true);
300+
// active facet lens (0..5 = a FACET_AXES_UI axis, or null = colour by class).
301+
const [facetAxis, setFacetAxis] = useState<number | null>(null);
276302

277303
// Fetch + decode the SoA once.
278304
useEffect(() => {
@@ -318,14 +344,22 @@ export function OsintGraph() {
318344
const { degree, touched, semantic } = view;
319345

320346
const baseSize = (i: number) => 11 + Math.min(degree.get(i) ?? 1, 16) * 1.5;
347+
// facet-lens colouring: when an axis is active, a node's border is its tenant
348+
// code on that axis (categorical, computed live across every node); else the
349+
// class colour. This is the dynamic group-by — no baked edges involved.
350+
const nodeBorder = (i: number) => {
351+
const ax = facetAxisRef.current;
352+
if (ax != null && soa.tenants) return facetColor(soa.tenants[i * 6 + ax]);
353+
return classColor(soa.cls[i]);
354+
};
321355
const baseNode = (i: number) => ({
322356
id: i,
323357
label: soa.labels[i] || `#${i}`,
324358
color: {
325359
background: 'rgba(10,14,23,0.88)',
326-
border: classColor(soa.cls[i]),
360+
border: nodeBorder(i),
327361
highlight: { background: 'rgba(10,14,23,0.96)', border: '#9fe8ff' },
328-
hover: { background: 'rgba(10,14,23,0.82)', border: classColor(soa.cls[i]) },
362+
hover: { background: 'rgba(10,14,23,0.82)', border: nodeBorder(i) },
329363
},
330364
size: baseSize(i),
331365
font: { color: '#d9e9f9' },
@@ -411,7 +445,7 @@ export function OsintGraph() {
411445
const brighten = (id: number) => {
412446
visNodes.update({
413447
id,
414-
color: { background: 'rgba(10,14,23,0.95)', border: classColor(soa.cls[id]) },
448+
color: { background: 'rgba(10,14,23,0.95)', border: nodeBorder(id) },
415449
font: { color: '#eaf4ff' },
416450
});
417451
};
@@ -611,9 +645,18 @@ export function OsintGraph() {
611645
visNodes.update(schemaNodeIds.map((id) => ({ id, hidden: !show })));
612646
visEdges.update(schemaEdgeIds.map((id) => ({ id, hidden: !show })));
613647
};
614-
// apply the current toggle state on (re)build — covers a toggle that landed
615-
// before the network (and apiRef) existed, so the button and graph never desync.
648+
// facet lens: recolour every rendered node by its tenant code on `axis`
649+
// (the dynamic group-by across all nodes); null restores the class colours.
650+
// Read-only over the tenant column — no edges, no relayout.
651+
const setFacet = (axis: number | null) => {
652+
facetAxisRef.current = axis != null && soa.tenants ? axis : null;
653+
visNodes.update(Array.from(touched).map(baseNode));
654+
};
655+
// apply the current toggle/lens state on (re)build — covers a toggle that
656+
// landed before the network (and apiRef) existed, so the buttons and graph
657+
// never desync.
616658
setDims(showDims);
659+
setFacet(facetAxis);
617660

618661
apiRef.current = {
619662
query: (text) => {
@@ -641,6 +684,7 @@ export function OsintGraph() {
641684
setReadout(null);
642685
},
643686
setDims,
687+
setFacet,
644688
};
645689

646690
net.on('click', (params: { nodes: unknown[] }) => {
@@ -674,6 +718,43 @@ export function OsintGraph() {
674718
setShowDims(next);
675719
apiRef.current?.setDims(next);
676720
};
721+
const toggleFacet = (axis: number) => {
722+
const next = facetAxis === axis ? null : axis;
723+
setFacetAxis(next);
724+
apiRef.current?.setFacet(next);
725+
};
726+
727+
// live legend for the active facet lens: value→count computed across every
728+
// rendered node from the tenant column, named via the materialized facet
729+
// edges (the two layers reinforcing each other). airo:type is a bitset, so its
730+
// codes read as raw role-masks rather than single values.
731+
const facetLegend = useMemo(() => {
732+
if (!soa || !soa.tenants || facetAxis == null || !view) return null;
733+
const tenants = soa.tenants;
734+
const axis = facetAxis;
735+
const rel = 10 + axis;
736+
const name = new Map<number, string>();
737+
for (const e of soa.edges) {
738+
if (e.r === rel && e.s < soa.nodeCount && e.t < soa.nodeCount) {
739+
const code = tenants[e.s * 6 + axis];
740+
if (code !== 0 && !name.has(code)) name.set(code, soa.labels[e.t] || `code ${code}`);
741+
}
742+
}
743+
const count = new Map<number, number>();
744+
let present = 0;
745+
view.touched.forEach((i) => {
746+
const code = tenants[i * 6 + axis];
747+
if (code !== 0) {
748+
count.set(code, (count.get(code) ?? 0) + 1);
749+
present += 1;
750+
}
751+
});
752+
const rows = Array.from(count.entries())
753+
.sort((a, b) => b[1] - a[1])
754+
.slice(0, 10)
755+
.map(([code, n]) => ({ code, n, label: name.get(code) ?? `code ${code}` }));
756+
return { rows, present, distinct: count.size };
757+
}, [soa, facetAxis, view]);
677758

678759
const lensChip = (i: number): CSSProperties => ({
679760
fontFamily: 'monospace',
@@ -805,13 +886,83 @@ export function OsintGraph() {
805886
</button>
806887
)}
807888
</div>
889+
{/* facet lens — colour every node by a tenant axis, the dynamic group-by */}
890+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, alignItems: 'center' }}>
891+
<span style={{ fontSize: 10, color: '#6f87a0', marginRight: 2 }}>◐ facet:</span>
892+
{FACET_AXES_UI.map((ax, i) => (
893+
<button
894+
key={ax}
895+
onClick={() => toggleFacet(i)}
896+
title={`colour every node by its ${ax} code — a live group-by across all nodes (the tenant column)`}
897+
style={{
898+
fontFamily: 'monospace',
899+
fontSize: 10,
900+
color: facetAxis === i ? '#0a0e17' : '#9fb4c8',
901+
background: facetAxis === i ? facetColor(i + 1) : 'rgba(17,32,48,0.6)',
902+
border: `1px solid ${facetColor(i + 1)}`,
903+
borderRadius: 6,
904+
padding: '4px 7px',
905+
cursor: 'pointer',
906+
fontWeight: facetAxis === i ? 700 : 400,
907+
}}
908+
>
909+
{ax}
910+
</button>
911+
))}
912+
</div>
808913
<div style={{ fontSize: 10, color: '#6f87a0' }}>
809914
one entity reasons from it; “A + B” traces the path. click any node to reason.
810915
</div>
811916
</div>
812917

813918
{readout && <ReasonBox readout={readout} onClose={clearReason} />}
814919

920+
{/* facet-lens legend — the live group-by over the tenant column */}
921+
{facetLegend && facetAxis != null && (
922+
<div
923+
style={{
924+
position: 'absolute',
925+
bottom: 16,
926+
right: 16,
927+
zIndex: 10,
928+
fontFamily: 'monospace',
929+
fontSize: 11,
930+
color: '#cfe7ff',
931+
background: 'rgba(8,12,20,0.86)',
932+
border: '1px solid #2a4a6a',
933+
borderRadius: 8,
934+
padding: '8px 10px',
935+
maxWidth: 230,
936+
maxHeight: '42%',
937+
overflowY: 'auto',
938+
pointerEvents: 'auto',
939+
}}
940+
>
941+
<div style={{ color: '#7fd1ff', marginBottom: 5 }}>
942+
{FACET_AXES_UI[facetAxis]} · {facetLegend.present} nodes · {facetLegend.distinct} values
943+
</div>
944+
{facetLegend.rows.map((r) => (
945+
<div
946+
key={r.code}
947+
style={{ display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap' }}
948+
>
949+
<span
950+
style={{
951+
display: 'inline-block',
952+
width: 9,
953+
height: 9,
954+
borderRadius: 9,
955+
border: `2px solid ${facetColor(r.code)}`,
956+
flex: '0 0 auto',
957+
}}
958+
/>
959+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.label}</span>
960+
<span style={{ color: '#7f97b0', marginLeft: 'auto' }}>{r.n}</span>
961+
</div>
962+
))}
963+
</div>
964+
)}
965+
815966
{/* class legend */}
816967
<div
817968
style={{
5.42 KB
Binary file not shown.

0 commit comments

Comments
 (0)