feat(data-plane): add bounded Gelios telemetry contract

This commit is contained in:
Codex
2026-07-22 11:47:24 +03:00
parent a9b8d71968
commit 4402c9ed25
16 changed files with 432 additions and 13 deletions
+1
View File
@@ -9,6 +9,7 @@ COPY packages/external-provider-contract/src/data-product.mjs ./packages/externa
COPY packages/external-provider-contract/src/geometry.mjs ./packages/external-provider-contract/src/geometry.mjs
COPY packages/external-provider-contract/src/intake-batch.mjs ./packages/external-provider-contract/src/intake-batch.mjs
COPY packages/external-provider-contract/src/sensitive-field-policy.mjs ./packages/external-provider-contract/src/sensitive-field-policy.mjs
COPY packages/external-provider-contract/src/telemetry-readings.mjs ./packages/external-provider-contract/src/telemetry-readings.mjs
COPY services/external-data-plane/package.json services/external-data-plane/package-lock.json ./services/external-data-plane/
WORKDIR /workspace/services/external-data-plane
@@ -0,0 +1,49 @@
{
"id": "fleet.positions.current.v5",
"version": "5.0.0",
"ontologyRevision": "ontology.map.moving_object.v3",
"deliveryMode": "snapshot+patch",
"semanticTypes": [
"map.moving_object"
],
"fields": [
"course_degrees",
"display_name",
"elevation_meters",
"engine_hours",
"geometry",
"hdop",
"horizontal_accuracy_meters",
"mileage_km",
"movement_state",
"object_kind",
"position_source",
"satellite_count",
"sensor_readings",
"signal_state",
"speed_kph"
],
"fieldContracts": {
"course_degrees": { "type": "number", "required": false, "minimum": 0, "maximum": 360 },
"display_name": { "type": "string", "required": true },
"elevation_meters": { "type": "number", "required": false },
"engine_hours": { "type": "number", "required": false, "minimum": 0 },
"geometry": { "type": "point", "required": false },
"hdop": { "type": "number", "required": false, "minimum": 0 },
"horizontal_accuracy_meters": { "type": "number", "required": false, "minimum": 0 },
"mileage_km": { "type": "number", "required": false, "minimum": 0 },
"movement_state": { "type": "string", "required": true, "enum": ["moving", "stopped"] },
"object_kind": { "type": "string", "required": true },
"position_source": { "type": "string", "required": true },
"satellite_count": { "type": "number", "required": false, "minimum": 0 },
"sensor_readings": { "type": "telemetry_readings", "required": false },
"signal_state": { "type": "string", "required": true, "enum": ["active", "inactive"] },
"speed_kph": { "type": "number", "required": false, "minimum": 0 }
},
"history": {
"mode": "sampled",
"intervalMs": 60000,
"strategy": "latest-per-entity-per-bucket",
"retentionDays": 90
}
}
@@ -4,6 +4,7 @@ import {
DATA_PRODUCT_PATCH_SCHEMA_VERSION,
DATA_PRODUCT_SNAPSHOT_SCHEMA_VERSION,
isBoundedGeoJsonGeometry,
isBoundedTelemetryReadings,
} from "@nodedc/external-provider-contract/data-plane";
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
@@ -854,6 +855,7 @@ function assertFieldContractValue(value, contract) {
function fieldContractValueMatchesType(value, type) {
if (type === "string_array") return Array.isArray(value) && value.every((item) => typeof item === "string");
if (type === "telemetry_readings") return isBoundedTelemetryReadings(value);
if (type === "point") return isBoundedGeoJsonGeometry(value, new Set(["Point"]));
if (type === "geometry") return isBoundedGeoJsonGeometry(value);
return typeof value === type && (type !== "number" || Number.isFinite(value));
@@ -1,10 +1,10 @@
import { isBoundedGeoJsonGeometry } from "@nodedc/external-provider-contract/data-plane";
import { isBoundedGeoJsonGeometry, isBoundedTelemetryReadings } from "@nodedc/external-provider-contract/data-plane";
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
const DELIVERY_MODES = new Set(["snapshot", "snapshot+patch", "query"]);
const HISTORY_MODES = new Set(["none", "all", "sampled"]);
const FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "point", "geometry"]);
const FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "telemetry_readings", "point", "geometry"]);
const DEFINITION_KEYS = new Set([
"id", "version", "ontologyRevision", "deliveryMode", "semanticTypes", "fields", "fieldContracts", "history",
]);
@@ -65,7 +65,7 @@ function normalizeFieldContract(value) {
if (value.enum !== undefined) {
if (!Array.isArray(value.enum) || value.enum.length === 0
|| new Set(value.enum.map(stableLiteral)).size !== value.enum.length
|| new Set(["point", "geometry", "string_array"]).has(type)
|| new Set(["point", "geometry", "string_array", "telemetry_readings"]).has(type)
|| value.enum.some((item) => !fieldContractValueMatchesType(item, type))) {
throw policyError("data_product_field_contract_enum_invalid");
}
@@ -174,6 +174,7 @@ function hasOnlyKeys(value, allowed) {
function fieldContractValueMatchesType(value, type) {
if (type === "string_array") return Array.isArray(value) && value.every((item) => typeof item === "string");
if (type === "telemetry_readings") return isBoundedTelemetryReadings(value);
if (type === "point") return isBoundedGeoJsonGeometry(value, new Set(["Point"]));
if (type === "geometry") return isBoundedGeoJsonGeometry(value);
return typeof value === type && (type !== "number" || Number.isFinite(value));
@@ -65,6 +65,40 @@ assert.throws(
}, definition),
(error) => error?.status === 422 && error?.code === "data_product_field_type_invalid",
);
const telemetryDefinition = normalizeDataProductDefinition({
id: "test.telemetry.current.v1",
version: "1.0.0",
ontologyRevision: "ontology.test.telemetry.v1",
deliveryMode: "snapshot+patch",
semanticTypes: ["map.moving_object"],
fields: ["sensor_readings"],
fieldContracts: {
sensor_readings: { type: "telemetry_readings", required: false },
},
history: { mode: "none", retentionDays: 1 },
});
const validReadings = [{
id: "battery.voltage",
label: "Battery voltage",
value: 51.7,
unit: "V",
observedAt: "2026-07-22T08:00:00.000Z",
}];
assert.doesNotThrow(() => assertPublishMatchesDefinition({ facts: [{
sourceId: "unit-telemetry",
semanticType: "map.moving_object",
observedAt: "2026-07-22T08:00:00.000Z",
attributes: { sensor_readings: validReadings },
}] }, telemetryDefinition));
assert.throws(
() => assertPublishMatchesDefinition({ facts: [{
sourceId: "unit-telemetry",
semanticType: "map.moving_object",
attributes: { sensor_readings: [{ ...validReadings[0], value: { raw: 51.7 } }] },
}] }, telemetryDefinition),
(error) => error?.status === 422 && error?.code === "data_product_field_type_invalid",
);
assert.throws(
() => assertPublishMatchesDefinition({
facts: [{ ...fact, attributes: { ...fact.attributes, speed_kph: -1 } }],
@@ -95,6 +95,15 @@ assert.equal(Object.isFrozen(strict.fieldContracts), true);
assert.equal(Object.isFrozen(strict.fieldContracts.signal_state), true);
assert.equal(Object.isFrozen(strict.fieldContracts.signal_state.enum), true);
const telemetry = normalizeDataProductDefinition({
...input,
fields: ["sensor_readings"],
fieldContracts: {
sensor_readings: { type: "telemetry_readings", required: false },
},
});
assert.equal(telemetry.fieldContracts.sensor_readings.type, "telemetry_readings");
assert.throws(
() => normalizeDataProductDefinition({
...input,
@@ -11,6 +11,7 @@ assert.deepEqual(bundled.map((definition) => definition.id), [
"fleet.positions.current.v2",
"fleet.positions.current.v3",
"fleet.positions.current.v4",
"fleet.positions.current.v5",
"map.zones.current.v1",
"map.zones.current.v2",
]);
@@ -47,19 +48,23 @@ assert.deepEqual(bundled[3].fieldContracts.movement_state, {
required: true,
enum: ["moving", "stopped"],
});
assert.equal(bundled[4].version, "1.0.0");
assert.equal(bundled[4].ontologyRevision, "ontology.map.zone.v1");
assert.deepEqual(bundled[4].semanticTypes, ["map.zone"]);
assert.deepEqual(bundled[4].fieldContracts.geometry, { type: "geometry", required: true });
assert.equal(bundled[4].fields.includes("dataset_authority"), false);
assert.equal(bundled[5].version, "2.0.0");
assert.equal(bundled[4].version, "5.0.0");
assert.equal(bundled[4].ontologyRevision, "ontology.map.moving_object.v3");
assert.equal(bundled[4].fieldContracts.sensor_readings.type, "telemetry_readings");
assert.equal(bundled[5].version, "1.0.0");
assert.equal(bundled[5].ontologyRevision, "ontology.map.zone.v1");
assert.deepEqual(bundled[5].semanticTypes, ["map.zone"]);
assert.deepEqual(bundled[5].fieldContracts.geometry, { type: "geometry", required: true });
assert.deepEqual(bundled[5].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]);
assert.equal(bundled[5].fields.includes("dataset_authority"), false);
assert.equal(bundled[6].version, "2.0.0");
assert.equal(bundled[6].ontologyRevision, "ontology.map.zone.v1");
assert.deepEqual(bundled[6].semanticTypes, ["map.zone"]);
assert.deepEqual(bundled[6].fieldContracts.geometry, { type: "geometry", required: true });
assert.deepEqual(bundled[6].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]);
assert.equal(Object.keys(bundled[3].fieldContracts).length, bundled[3].fields.length);
assert.equal(Object.keys(bundled[4].fieldContracts).length, bundled[4].fields.length);
assert.equal(Object.keys(bundled[5].fieldContracts).length, bundled[5].fields.length);
assert.equal(Object.keys(bundled[6].fieldContracts).length, bundled[6].fields.length);
const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-"));
try {