Skip to content

Commit f7fc518

Browse files
abrignoniclaude
andauthored
Resolve knowledge-graph event locations to place and coordinates (abrignoni#2265)
The Events artifact previously reported an event's location only as the raw 'md:<id>' pointer stored in the graph. That id is the modeled place node's subject; reading it back gives the place the graph inferred. Resolve each location visit to its place by looking the referenced node up in the entity tables (stable_graph, then expired_stable_graph) and add Location Name, Location Address, Latitude and Longitude columns. Because the events now carry coordinates, enable KML so a location-visit timeline plots on a map. Events with no location leave the new columns blank. Row selection is unchanged, so the recorded row counts still hold. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 03734bf commit f7fc518

1 file changed

Lines changed: 74 additions & 8 deletions

File tree

‎scripts/artifacts/intelligencePlatformGraph.py‎

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"name": "Intelligence Platform Knowledge Graph - Events",
5555
"description": "Dated events the on-device knowledge graph inferred, such as "
5656
"location visits and calendar events, with their imputed start "
57-
"and end times.",
57+
"and end times, and for a location visit the referenced place "
58+
"resolved to a name, address and coordinates.",
5859
"author": "@AlexisBrignoni, Claude",
5960
"creation_date": "2026-09-23",
6061
"last_update_date": "2026-09-23",
@@ -71,18 +72,24 @@
7172
"not reported. Predicate and class codes "
7273
"are resolved from the ontology.db shipped in the same folder. Events are "
7374
"inferred by the system, so an event is not evidence the user was present "
74-
"or confirmed it. The location reference is reported as stored and is not "
75-
"resolved to a place name here. Confidence is the value the graph "
76-
"recorded, not a measurement made here, and can hold one value across "
77-
"every event on a device that stored few of them. Retired events are "
78-
"read from the expired_event_graph table alongside the live event_graph "
79-
"and flagged in the Expired column. The IntelligencePlatform graph "
75+
"or confirmed it. The location reference is reported as stored (an "
76+
"'md:<id>' pointer whose number is the modeled place node's id) and is "
77+
"also resolved here to that place's name, composed address, latitude and "
78+
"longitude by reading the referenced node from the entity tables "
79+
"(stable_graph, then expired_stable_graph). A location visit therefore "
80+
"carries coordinates and is written to KML; calendar and other events "
81+
"with no location leave the location columns blank. Confidence is the "
82+
"value the graph recorded, not a measurement made here, and can hold one "
83+
"value across every event on a device that stored few of them. Retired "
84+
"events are read from the expired_event_graph table alongside the live "
85+
"event_graph and flagged in the Expired column. The IntelligencePlatform "
86+
"graph "
8087
"databases were described by 0x11 Forensics and Consulting, 'That is one "
8188
"smart Apple', 0x11forensicssc.com, 2026-07-21.",
8289
"paths": (
8390
'*/mobile/Library/IntelligencePlatform/graph.db*',
8491
'*/mobile/Library/IntelligencePlatform/ontology.db*'),
85-
"output_types": "standard",
92+
"output_types": "all",
8693
"artifact_icon": "map-pin",
8794
"sample_data": {
8895
"dexter_ios18": "iOS 18.3.2 | 1032 rows",
@@ -244,6 +251,55 @@ def _cocoa(value):
244251
return None
245252

246253

254+
def _first(values):
255+
"""Return the first non-empty value as text, else ''."""
256+
for value in values:
257+
text = '' if value is None else str(value)
258+
if text:
259+
return text
260+
return ''
261+
262+
263+
def _place_lookup(files_found, predicates):
264+
"""Map each modeled node's subject id to its resolved (name, composed address,
265+
latitude, longitude), gathered from the live and expired entity tables. An event
266+
references its location by node id (object 'md:<id>'), so this turns that pointer
267+
into the place the graph modeled. The live stable_graph wins over the expired
268+
table when a node is in both."""
269+
lookup = {}
270+
for table_name in ("stable_graph", "expired_stable_graph"):
271+
rows, _ = _read_triples(files_found, table_name)
272+
for subject, ent in _group_by_subject(rows, predicates).items():
273+
number = _first(_part(ent, 'has address', 'sub thoroughfare'))
274+
street = _first(_part(ent, 'has address', 'full street address'))
275+
city = _first(_part(ent, 'has address', 'city'))
276+
region = _first(_part(ent, 'has address', 'administrative district'))
277+
postal = _first(_part(ent, 'has address', 'postal code'))
278+
name = _first(_part(ent, 'has address', 'name'))
279+
lat, lon = _coords(ent)
280+
line1 = ' '.join(p for p in (number, street) if p)
281+
region_zip = ' '.join(p for p in (region, postal) if p)
282+
address = ', '.join(p for p in (line1, city, region_zip) if p)
283+
if subject not in lookup and (name or address or lat or lon):
284+
lookup[subject] = (name, address, lat, lon)
285+
return lookup
286+
287+
288+
def _resolve_location(ent, places):
289+
"""Resolve an event's location reference ('md:<id>') to the modeled place's
290+
(name, address, latitude, longitude). Returns blanks when the event has no
291+
location or the reference does not resolve to a modeled node."""
292+
for ref in _part(ent, 'has location relationship', 'has location'):
293+
if isinstance(ref, str) and ref.startswith('md:'):
294+
try:
295+
node = int(ref[3:])
296+
except ValueError:
297+
continue
298+
if node in places:
299+
return places[node]
300+
return '', '', '', ''
301+
302+
247303
@artifact_processor
248304
def intelligencePlatformEntities(context):
249305
files_found = context.get_files_found()
@@ -306,6 +362,7 @@ def intelligencePlatformEntities(context):
306362
def intelligencePlatformEvents(context):
307363
files_found = context.get_files_found()
308364
predicates, classes = _load_ontology(files_found)
365+
places = _place_lookup(files_found, predicates)
309366

310367
data_list = []
311368
source_path = ''
@@ -321,12 +378,17 @@ def intelligencePlatformEvents(context):
321378
ends = [_cocoa(v) for v in _part(ent, 'has date', 'imputed end time')]
322379
starts = [s for s in starts if s]
323380
ends = [e for e in ends if e]
381+
loc_name, loc_addr, loc_lat, loc_lon = _resolve_location(ent, places)
324382
data_list.append((
325383
min(starts) if starts else '',
326384
max(ends) if ends else '',
327385
_join(classes.get(v, v) for v in _base(ent, 'is a')),
328386
_join(_base(ent, 'name')),
329387
_join(_part(ent, 'has location relationship', 'has location')),
388+
loc_name,
389+
loc_addr,
390+
loc_lat,
391+
loc_lon,
330392
round(ent['confidence'], 4),
331393
expired,
332394
str(subject)))
@@ -337,6 +399,10 @@ def intelligencePlatformEvents(context):
337399
'Event Type',
338400
'Name',
339401
'Location (as stored)',
402+
'Location Name',
403+
'Location Address',
404+
'Latitude',
405+
'Longitude',
340406
'Confidence',
341407
'Expired',
342408
'Event ID')

0 commit comments

Comments
 (0)