From 9261211ab314af6f8986b3ab42b959c870ce6f0b Mon Sep 17 00:00:00 2001 From: Egor Yakovlev Date: Tue, 27 Oct 2026 12:00:18 +0100 Subject: [PATCH] Add Track and GeometryEncoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two platforms had independently written byte-identical route blocks inside differently-named envelopes: W3DS Health's Workout (polyline5 route) and Meshenger's live location share. Neither is registered, so a reader arriving at somebody's vault cannot tell that one shape serves both, and must know which application wrote which record in order to find a route at all. Track carries the geometry and says nothing about why the thing moved. A run, a commute and a parcel delivery are the same geometry; the meaning belongs to whatever record points at the track. Its subject is a THING rather than an eName, because a parcel's movement belongs to no account and narrowing it to people is what forced the duplicate shapes. GeometryEncoding is a published list in the same style as Domain — the list itself, not a copy of it. GeoJSON is canonical, as FieldBoundary already uses, and polyline5 is declared as an encoding OF it rather than as a peer, with its mapping stated: lossy at five decimals, and latitude-first where GeoJSON is longitude-first. That axis difference does not fail loudly; it places a point in another hemisphere, so it is written down once here instead of being rediscovered by each reader. Separating the encoding from the shape rather than embedding it: an encoding is reusable by more than one shape (a polyline can encode a polygon ring), so adding one stays a single change to a single file. Two rules live with the list because they generalise past geometry. A reader may REFUSE an encoding it does not know, but must never REWRITE the declared value — a record then decodes as something it never claimed to be. And pointCount is required so that a reader which cannot decode the data still knows whether there was anything to decode: an unknown encoding with pointCount above zero must say so rather than render as empty, because an empty drawing is indistinguishable from an absent one. Checked against the repository's own rules: no duplicate schemaId across the 73 schemas, both domains exist in domain.json, both files parse. --- .../ontology/schemas/geometryEncoding.json | 29 +++++++ services/ontology/schemas/track.json | 78 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 services/ontology/schemas/geometryEncoding.json create mode 100644 services/ontology/schemas/track.json diff --git a/services/ontology/schemas/geometryEncoding.json b/services/ontology/schemas/geometryEncoding.json new file mode 100644 index 000000000..e0cdf0bf2 --- /dev/null +++ b/services/ontology/schemas/geometryEncoding.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://ontology.w3ds.metastate.foundation/schemas/504cacb9-79ef-498c-a352-e17830ed1876", + "schemaId": "504cacb9-79ef-498c-a352-e17830ed1876", + "title": "GeometryEncoding", + "domain": "governance", + "type": "object", + "description": "How a geometry is written down, as opposed to what it means. GeoJSON is the canonical model in this ecosystem — FieldBoundary already uses it — and every other entry here is an ENCODING of that model, with its mapping stated. The permitted values are the `oneOf` list under `id`: this schema is the published list, not a copy of it, in the same way as Domain. Two rules that belong with the list rather than with any one shape. A reader may REFUSE an encoding it does not know; it must never REWRITE the declared value, because a record then decodes as something it never claimed to be. And a reader that meets an unknown encoding while `pointCount` is greater than zero must say so — 'geometry in encoding X, not supported' — and must never render as if the geometry were empty, because an empty drawing is indistinguishable from an absent one.", + "properties": { + "id": { + "type": "string", + "description": "Stable encoding id, used in the `format` field of any shape that carries a geometry.", + "oneOf": [ + { + "const": "geojson", + "title": "GeoJSON", + "description": "The canonical model, RFC 7946. Coordinates are [lon, lat] with an optional third element for elevation in metres, in WGS84. Lossless: the numbers stored are the numbers meant. Axis order is LONGITUDE FIRST." + }, + { + "const": "polyline5", + "title": "Encoded Polyline, precision 5", + "description": "Google's Encoded Polyline Algorithm Format at 1e-5 precision, referenced rather than restated here. Maps to a GeoJSON LineString. AXIS ORDER IS LATITUDE FIRST, which is the opposite of GeoJSON, so a reader translates in one direction only and a confusion between the two is not a small error — it places a point in another hemisphere without failing. LOSSY: coordinates are rounded to five decimal places, about 1.1 m, so a decoded point must never be compared with its original for equality. Chosen by existing data rather than by design: imported health Workouts and Meshenger location tracks already hold it, and neither can be rewritten on vaults their platform does not own." + } + ] + } + }, + "required": ["id"], + "additionalProperties": false +} diff --git a/services/ontology/schemas/track.json b/services/ontology/schemas/track.json new file mode 100644 index 000000000..2f4f157e4 --- /dev/null +++ b/services/ontology/schemas/track.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://ontology.w3ds.metastate.foundation/schemas/dbddf08e-7a18-4c6f-b171-b5acc6b21b6f", + "schemaId": "dbddf08e-7a18-4c6f-b171-b5acc6b21b6f", + "title": "Track", + "domain": "mobility", + "type": "object", + "description": "The movement of ONE THING over time: a person walking, a parcel in transit, a vehicle, a shared live position. Deliberately says nothing about WHY the thing moved — a run, a commute and a delivery are the same geometry, and the meaning belongs to whatever record points at the track. That separation is the whole purpose: two platforms had independently written byte-identical route blocks inside differently-named envelopes (a health Workout and a messenger's location share), so no reader arriving at a vault could tell that one shape served both. A record that owns the geometry and is its only owner may embed this shape inline instead of referencing a Track envelope; see `route`.", + "properties": { + "id": { "type": "string", "description": "Envelope id." }, + "subject": { + "type": "string", + "description": "The w3id of the THING that moved — a person, a parcel, a vehicle. Not necessarily an eName: a parcel's movement belongs to no account, and narrowing this to people is what forced the duplicate shapes in the first place." + }, + "agent": { + "type": "string", + "description": "eName of whoever RECORDED the movement, when that is not the subject. A phone recording its owner, a carrier recording a parcel." + }, + "startedAt": { "type": "string", "format": "date-time", "description": "When recording began." }, + "endedAt": { + "type": "string", + "format": "date-time", + "description": "When recording finished. Absent while it is still being appended to. NOTE that this is about the RECORDING and not about any permission: if the track exists because somebody shared their position for a window of time, that window lives on the record that points here, not on the track, so that one fact has one home." + }, + "route": { + "type": "object", + "description": "The geometry itself. `format` names an entry in GeometryEncoding; `pointCount` is REQUIRED and is what separates 'empty' from 'could not decode', which is the only defence a reader has when it meets an encoding it does not implement.", + "properties": { + "format": { + "type": "string", + "description": "An `id` from GeometryEncoding (504cacb9-79ef-498c-a352-e17830ed1876). Refuse one you do not know; never rewrite it." + }, + "data": { + "type": "string", + "description": "The geometry, written in `format`. For geojson, the JSON text of a LineString; for polyline5, the encoded string." + }, + "pointCount": { + "type": "integer", + "minimum": 0, + "description": "How many points the geometry holds. Required so that a reader which cannot decode `data` still knows whether there was anything to decode. Zero means genuinely empty." + } + }, + "required": ["format", "data", "pointCount"], + "additionalProperties": false + }, + "samples": { + "type": "object", + "description": "Per-point measurements, each array the SAME LENGTH as `pointCount` and in the same order. A samples array out of step with the geometry pairs one moment's speed with another place, silently, so a reader checks the lengths rather than trusting them. An absent array means not measured; a null entry means not measured at that point — never write zero or a default for an unknown, which would be indistinguishable from a real reading.", + "properties": { + "t": { + "type": "array", + "items": { "type": "number" }, + "description": "Seconds since `startedAt` for each point." + }, + "speed": { + "type": "array", + "items": { "type": ["number", "null"] }, + "description": "Metres per second, as the device reported it." + }, + "heading": { + "type": "array", + "items": { "type": ["number", "null"] }, + "description": "Degrees true." + }, + "ele": { + "type": "array", + "items": { "type": ["number", "null"] }, + "description": "Metres above the WGS84 ellipsoid." + } + }, + "additionalProperties": false + }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["id", "subject", "startedAt", "route"], + "additionalProperties": true +}