Conventions
- Encoding. Files are UTF-8; a byte-order mark is tolerated. The viewer also reads UTF-16, which is what Windows PowerShell writes when output is redirected with
>. - Identifiers. Entity identifiers are UUIDs written as strings. Identifiers in case graphs are derived from the case itself, so the same case always produces the same graph.
- Times. Times are RFC 3339 strings with a time zone, such as
2026-09-30T21:00:00Zor2026-09-30T22:00:00+01:00. - Coordinates. Positions are WGS 84 degrees, latitude from −90 to 90 and longitude from −180 to 180. GeoJSON coordinates are written longitude first, as RFC 7946 requires.
- Names of things. Kinds and modes are written as their names, such as
"Observer"or"Geodesic". The order of keys inside an object carries no meaning.
This page covers the files SkeleTrace writes, and the files you prepare for the case notebook and the landscape. The engine’s own configuration (profiles, view jobs, filters, saved queries and watchlists) is outside its scope.
Command output
Every skeletrace command prints a single JSON document. Results are wrapped in a named envelope that says what they are, so the viewer and any script can unwrap them by name.
| Command | Envelope | Contents |
|---|---|---|
materialize-topology | {"Topology": …} | Topology view |
case graph | {"Topology": …} | Topology view of a case |
materialize-sparse-geo | {"SparseGeo": …} | Sparse-geo map |
analyze-topology | {"TopologyAnalysis": …} | Bottleneck report |
case bottlenecks | {"Case": {"Bottlenecks": …}} | Bottleneck report |
case show | {"Case": {"Document": …}} | Case document |
case export --format json | None | Case document, written to the file named |
landscape-report, verify-packets | {"Landscape": …} | Landscape report |
Other case commands answer under Case, with names such as RunSaved, Runs, Observation, Linked and Exported.
Shared building blocks
Position
{ "lat": 51.481, "lon": -3.179, "alt": null }
| Field | Type | Meaning |
|---|---|---|
lat | number | Latitude in degrees, −90 to 90. |
lon | number | Longitude in degrees, −180 to 180. |
alt | number or null | Altitude, where known. |
Extent
A rectangle on the map, used for boundaries and for the viewport of a sparse-geo map.
{ "min_lat": 35.0, "min_lon": -11.0, "max_lat": 62.0, "max_lon": 30.0 }
Metric value
As it appears in topology views:
{
"metric_id": "5f0c2a1e-8d3b-4c55-9e1a-2b7c4d6e8f90",
"metric_name": "latency",
"display_value": "42.0",
"unit": "ms",
"timestamp": "2026-09-30T21:00:00Z"
}
display_value is text, already formatted for reading. timestamp is the time of the sample shown.
Kinds and modes
- Node kinds
Endpoint,Hop,Relay,Observer,Anchor,Exchange,Identity,Account,Topic,Cluster,Chokepoint,Unknown- Edge kinds
Route,Link,Inference,Association,Membership,Reference,BoundaryCrossing- Geometry modes
Straight,Geodesic,RaisedArc,ThroughGlobe,Abstract; see place and geometry
Topology view
Written by materialize-topology and case graph.
{
"Topology": {
"view_id": "b8e5e04c-13de-55f0-97b3-8f8f46327759",
"nodes": [
{
"entity_id": "4a4c857b-fe5c-5dd7-8c9b-b91f4e5ac455",
"label": "Cardiff observer",
"kind_label": "Observer",
"position": { "lat": 51.481, "lon": -3.179, "alt": null },
"metrics": []
},
{
"entity_id": "662fc329-bc88-5f6b-ab76-4f34cb036591",
"label": "London exchange",
"kind_label": "Exchange",
"position": { "lat": 51.507, "lon": -0.128, "alt": null },
"metrics": [
{
"metric_id": "5f0c2a1e-8d3b-4c55-9e1a-2b7c4d6e8f90",
"metric_name": "throughput",
"display_value": "3.1",
"unit": "Tbit/s",
"timestamp": "2026-09-30T21:00:00Z"
}
]
}
],
"edges": [
{
"entity_id": "18af9bb0-d3a4-58e8-9bd6-2b67e6bca3d0",
"source": "4a4c857b-fe5c-5dd7-8c9b-b91f4e5ac455",
"target": "662fc329-bc88-5f6b-ab76-4f34cb036591",
"kind_label": "Route",
"geometry_mode": "Straight",
"metrics": []
}
],
"boundaries": [
{
"entity_id": "0d6c1b8e-6a4f-4f4e-9b1e-3c2d5e7f9a10",
"label": "EU jurisdiction",
"kind_label": "Legal",
"extent": { "min_lat": 35.0, "min_lon": -11.0, "max_lat": 62.0, "max_lon": 30.0 },
"metrics": []
}
]
}
}
| Field | Meaning |
|---|---|
view_id | Identifier of the view that produced this file. |
nodes[].entity_id | The node’s identifier. |
nodes[].label | A human-readable name. |
nodes[].kind_label | One of the node kinds. |
nodes[].position | A position, or null when the node has none. |
nodes[].metrics | Latest metric values for the node. |
edges[].source, edges[].target | The entity_id of each end. |
edges[].kind_label | One of the edge kinds. |
edges[].geometry_mode | How the link is drawn on maps. |
boundaries[].extent | An extent, or null when the boundary has no geography. |
Case graphs use the same shape. Sources appear as Observer nodes, observations of people and organisations as Identity nodes and observations of domains as Endpoint nodes. Each observation has a Reference link to its source, and each reviewed link appears as an Association.
Sparse-geo map
Written by materialize-sparse-geo. The map itself is a standard GeoJSON feature collection, wrapped with the viewport it was drawn for.
{
"SparseGeo": {
"viewport": { "min_lat": 30.0, "min_lon": -80.0, "max_lat": 60.0, "max_lon": 10.0 },
"feature_count": 2,
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "4a4c857b-fe5c-5dd7-8c9b-b91f4e5ac455",
"geometry": { "type": "Point", "coordinates": [-3.179, 51.481] },
"properties": {
"label": "Cardiff observer",
"kind": "Observer",
"metrics": {
"latency": {
"metric_id": "5f0c2a1e-8d3b-4c55-9e1a-2b7c4d6e8f90",
"value": "11",
"unit": "ms",
"timestamp": "2026-09-30T21:00:00+00:00"
}
}
}
},
{
"type": "Feature",
"id": "18af9bb0-d3a4-58e8-9bd6-2b67e6bca3d0",
"geometry": { "type": "LineString", "coordinates": [[-3.179, 51.481], [-74.006, 40.713]] },
"properties": { "label": "Transatlantic route", "kind": "Route", "metrics": {} }
}
]
}
}
}
- A feature’s
idis the entity’s identifier. - Nodes are
Pointfeatures and routes areLineStringfeatures. Extents are written as closed rectangularPolygonfeatures. properties.metricsis an object keyed by metric name; each entry holdsmetric_id,value,unitandtimestamp.- The viewer joins a line to the points at its two ends when their coordinates match, and treats it as a link between them; other lines are drawn as tracks.
Bottleneck report
Written by analyze-topology and case bottlenecks. In the example, one group of four is shown.
{
"Case": {
"Bottlenecks": {
"node_count": 20,
"edge_count": 12,
"components": [
[
"4a4c857b-fe5c-5dd7-8c9b-b91f4e5ac455",
"662fc329-bc88-5f6b-ab76-4f34cb036591",
"3028c9b9-bc7a-5166-8700-84e820ac1817",
"707e8899-14b5-5ef4-8f07-81b4321c70ee"
]
],
"chokepoints": [
{
"entity_id": "662fc329-bc88-5f6b-ab76-4f34cb036591",
"label": "#1 Avery Example",
"kind_label": "Identity",
"degree": 2,
"splits_into": 2
}
],
"bridges": [
{
"entity_id": "18af9bb0-d3a4-58e8-9bd6-2b67e6bca3d0",
"source": "662fc329-bc88-5f6b-ab76-4f34cb036591",
"target": "4a4c857b-fe5c-5dd7-8c9b-b91f4e5ac455",
"kind_label": "Reference"
}
],
"dangling_edges": []
}
}
}
| Field | Meaning |
|---|---|
node_count, edge_count | The size of the view analysed. |
components | Groups of connected nodes, largest first, as lists of entity identifiers. |
chokepoints | Nodes whose removal splits their group, the most disruptive first. degree counts the node’s links; splits_into is the number of pieces its removal leaves. |
bridges | Links whose loss splits their group. |
dangling_edges | Links whose ends are not both in the view. They are left out of the analysis. |
A report holds no positions and only some of the links. Opened on its own, the viewer shows what it can; dropped onto the topology view it was made from, the viewer compares the report with its own analysis.
Case document
Written by case show, and without the envelope by case export --format json. The same document is produced by the Python OSINTropy tool. One observation is shown.
{
"Case": {
"Document": {
"id": "b626be92cc674c5b89df55763f8e772c",
"query": "Avery Example",
"created_at": "2026-10-01T06:36:03+00:00",
"status": "complete",
"errors": {},
"observations": [
{
"id": 1,
"source": "npi",
"source_id": "1234567890",
"name": "Avery Example",
"fields": { "practice_address": ["1 Practice St, Sampleton, CA, 12345"] },
"source_url": "https://npiregistry.cms.hhs.gov/provider-view/1234567890",
"kind": "record",
"subject_type": "person",
"collected_at": "2026-10-01T06:36:03+00:00"
}
],
"reviewed_links": [
{ "a": 1, "b": 2, "reason": "Same NPI number on both records", "created_at": "2026-10-01T06:40:12+00:00" }
],
"analysis": {
"observation_count": 2,
"record_count": 2,
"lead_count": 0,
"source_counts": { "npi": 1, "wikidata": 1 },
"source_diversity_bits": 1.0,
"reviewed_groups": [[1, 2]],
"possible_reviews": [],
"field_variations_in_reviewed_groups": [],
"unreviewed_ids": [],
"interpretation": "Source diversity measures distribution, not truth, identity, or confidence."
},
"method": "Each source hit remains separate until two observation IDs are explicitly linked after review."
}
}
}
The run
| Field | Meaning |
|---|---|
id | Run identifier, 32 hexadecimal characters. |
query | What was searched for or imported. |
status | complete; partial when some sources failed; failed when none returned anything. |
errors | Source name to error message, for the sources that failed. |
Observations
| Field | Meaning |
|---|---|
id | Observation number, unique within the case file. |
source, source_id | The source, and the identifier the source uses for this entry. |
name | The name as the source gives it. Leads are named after the search, for example Search lead for Avery Example. |
fields | Field name to a list of values, exactly as the source gave them. |
source_url | The public address of the entry, or an empty string. |
kind | record, lead or import. |
subject_type | person, organization or domain. |
collected_at | When the observation was collected. |
Links and analysis
Each reviewed link names two observations, a and b (the smaller number first), with its reason and the time it was made. The analysis repeats what the run shows: counts of observations, records and leads; observations per source; source diversity in bits; groups formed by reviewed links; the review queue (possible_reviews); fields that disagree within a group; and the observations not yet in any group.
Case imports
Read by case import-file. A JSON import is either a list of observations or an object with a query and an observations list.
{
"query": "Notes on Example Works Ltd",
"observations": [
{
"source": "companies_register_extract",
"source_id": "EW-0001",
"name": "Example Works Ltd",
"subject_type": "organization",
"source_url": "https://example.org/register/EW-0001",
"collected_at": "2026-09-01T10:00:00+01:00",
"fields": { "company_number": "EW-0001", "city": "Cardiff" }
}
]
}
| Field | Required | Rules |
|---|---|---|
source | Yes | Up to 100 characters. |
name | Yes | Up to 250 characters. |
source_id | No | Up to 250 characters. When it is missing, a fingerprint of the source, name, address and fields is used, so the same entry always receives the same identifier. |
kind | No | record, lead or import. Defaults to import. |
subject_type | No | person, organization or domain. Defaults to person. |
source_url | No | An http or https address with no user name or password in it. |
collected_at | No | A time with a time zone. Defaults to the time of import. |
fields | No | Up to 64 names. Each value is text, a number, or a list of up to 100 of them, each up to 4,096 characters. Empty and repeated values are dropped. |
All text is collapsed to single spaces. Control characters and the characters that reverse text direction are refused, so that nothing in a case can disguise itself. An import may hold up to 10,000 observations and 20 MB.
A CSV import needs a header row with source and name columns. The columns source_id, source_url, kind, subject_type and collected_at are read as above. Every other column becomes a field, and a fields_json column may carry further fields as a JSON object.
source,name,source_id,subject_type,source_url,collected_at,role,city
staff_directory,Avery Example,SD-17,person,https://example.org/staff/17,2026-09-01T09:30:00+01:00,Director,Cardiff
Landscape files
Report request
Read by landscape-report. Only mode is required; the other values shown are the defaults.
{
"mode": "replay",
"input": "captures/events.csv",
"speed": 1.0,
"rate": 4096,
"seed": 221909157,
"options": {
"tick_ms": 125,
"width": 120,
"height": 36,
"window": 256,
"max_alerts": 50,
"max_ticks": 28800,
"include_frame": true
}
}
| Field | Meaning |
|---|---|
mode | demo, replay (CSV or JSONL packet events), bytes (any file) or pcap-file (pcap or pcapng). |
input | Path to the input, relative to where the command runs. Required for every mode except demo. |
speed | Playback speed for replays and captures, above 0 and up to 100,000. |
rate | Bytes per second fed through the analysis in bytes mode. |
seed | Seed for the demonstration. |
options.tick_ms | Milliseconds per column, 20 to 5,000. |
options.width, options.height | Size of the drawn frame in characters: 20 to 1,000 wide, 8 to 500 high. |
options.window | Observations per lane used for the statistics, 32 to 4,096. |
options.max_alerts | Alerts kept, 1 to 10,000. |
options.max_ticks | Where to stop: 480 ticks (one minute) by default for the demonstration, 28,800 for files. |
options.include_frame | Whether to include the final frame as text. |
Verification request
Read by verify-packets: {"input_path": "captures/events.jsonl", "options": { … }}, with the same options as above.
Report
Both commands answer with a report under Landscape:
| Field | Meaning |
|---|---|
source_label, lane_set | What was read, and whether the lanes are packets or bytes. |
ticks, tick_ms, stream_seconds | How much stream time was covered. |
ended, truncated, warming_up | Whether the input ran out, whether the run stopped at its limit first, and whether the statistics had too little data to settle. |
total_events, total_bytes, bad_records, dropped | Totals, including malformed rows that were skipped. |
lanes | One entry per lane: totals, the current class and markers, entropy, dependence, entropy rate, rhythm, divergence from the lane’s history, rates, and counts of each column type. |
alerts | Newest first, each with its stream time, lane, kind (demon, shift, burst or rhythm) and message. |
frame | The final frame as plain text, when requested. |
Packet event files
Packet events can be CSV with a header row, JSONL with one object per line, or, for verify-packets, a JSON list. The fields are the same in every form: ts_ms, len, proto, src, dst, sport, dport and dir.
# JSONL: lines starting with # are ignored
{"ts_ms": 0, "len": 1500, "proto": "tcp", "src": "10.0.0.2", "dst": "93.184.216.34", "sport": 51000, "dport": 443}
{"ts_ms": 5, "len": 80, "proto": "udp", "sport": 53000, "dport": 53}
ts_msis a whole number of milliseconds. A row without it is placed 10 milliseconds after the row before.lenis the packet length in bytes. The protocol and ports decide the lane.- Time never runs backwards: a row stamped earlier than one already read is held at the latest time seen.
- Malformed rows are counted in
bad_recordsand skipped; they do not stop the run. verify-packetsloads files of up to 64 MiB. Larger files can be streamed withskeletrace landscape --mode replay.
Other files the viewer opens
- GeoJSON
- Points become nodes, lines between two points become links, other lines become tracks and polygons become areas. The properties
labelorname, andkind_labelorkind, are used when present. - GraphML
- Nodes, edges and their data keys. Case notebooks exported as GraphML open with sources, observations and leads told apart.
- Graph JSON
- An object with
nodesandedgesorlinks, usingid,labelandkindon nodes andsourceandtargeton links. Index references and Cytoscape element lists are accepted. - Observation lists
- A JSON list of observations, as used for imports, opens as a case.
A landscape report is not a graph, and the viewer says so rather than guessing.