feat(map): ingest audited department zone snapshot

This commit is contained in:
Codex
2026-07-21 12:02:11 +03:00
parent 3446f3edc2
commit 343812b90d
19 changed files with 76785 additions and 105 deletions
@@ -1,13 +1,8 @@
# Gelios provider package v6
Adds the bounded, offset-paged `gelios.geozone` read capability and maps a
complete accepted source generation into the provider-neutral
`map.zones.current.v1` Data Product. The runtime publisher must use replace
mode: a partial provider page set never reaches Publish, while a complete newer
generation atomically upserts present zones and emits remove patches for zones
that disappeared.
complete account generation into `map.gelios-geofences.current.v1`.
`gelios-rest-v1` and `mmap-snapshot-v1` are source adapters, not Foundry
bindings. Both must preserve the native Gelios zone id (or use an explicit
crosswalk) and produce the same `map.zone` facts. Foundry only consumes the
provider-neutral product.
Gelios account geo-objects are deliberately not published into the normative
`map.zones.current.v1` Department of Transport dataset. Independent authorities
must never share one replace generation.
@@ -2,7 +2,7 @@ import { geliosProviderPackageV5 } from "../v5/package.mjs";
export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v6";
export const GELIOS_PROVIDER_PACKAGE_VERSION = "6.0.0";
export const GELIOS_ZONES_DATA_PRODUCT_ID = "map.zones.current.v1";
export const GELIOS_ZONES_DATA_PRODUCT_ID = "map.gelios-geofences.current.v1";
export const GELIOS_ZONES_DATA_PRODUCT_VERSION = "1.0.0";
export const GELIOS_ZONES_ONTOLOGY_REVISION = "ontology.map.zone.v1";
export const GELIOS_ZONE_SOURCE_ID_PREFIX = "gelios-zone-";
@@ -10,7 +10,7 @@ export const GELIOS_ZONE_SOURCE_ID_PREFIX = "gelios-zone-";
const ZONES_CAPABILITY_ID = "gelios.geozones.current.read";
const ZONES_FIELD_POLICY_ID = "gelios.geozones.current.fields.v1";
const ZONES_PROFILE_ID = "gelios.geozones.current.realtime.v1";
const ZONES_MAPPING_ID = "gelios.geozones.to.map.zones.current.v1";
const ZONES_MAPPING_ID = "gelios.geozones.to.map.gelios-geofences.current.v1";
const ZONES_TEMPLATE_ID = "gelios.geozones.current.l2.v1";
const value = structuredClone(geliosProviderPackageV5);
@@ -2,10 +2,29 @@ import { createHash } from "node:crypto";
import { isBoundedGeoJsonGeometry } from "./geometry.mjs";
export const ZONE_SOURCE_GENERATION_SCHEMA_VERSION = "nodedc.zone-source-generation/v1";
export const ZONE_SOURCE_GENERATION_SCHEMA_VERSION = "nodedc.zone-source-generation/v2";
export const ZONE_SOURCE_ADAPTERS = Object.freeze({
"gelios-rest-v1": Object.freeze({ sourceKind: "live_api", sourceRevision: "gelios-rest-v1" }),
"mmap-snapshot-v1": Object.freeze({ sourceKind: "versioned_snapshot", sourceRevision: "mmap-snapshot-v1" }),
"depttrans-api-snapshot-v1": Object.freeze({
authorityId: "moscow-department-of-transport",
datasetId: "pmd-slow-zones",
sourceIdPrefix: "depttrans-pmd-slow-zone-",
sourceKind: "versioned_snapshot",
sourceRevision: "depttrans-api-snapshot-v1",
}),
"depttrans-mmap-snapshot-v1": Object.freeze({
authorityId: "moscow-department-of-transport",
datasetId: "pmd-slow-zones",
sourceIdPrefix: "depttrans-pmd-slow-zone-",
sourceKind: "versioned_snapshot",
sourceRevision: "depttrans-mmap-snapshot-v1",
}),
"gelios-rest-v1": Object.freeze({
authorityId: "gelios-account",
datasetId: "gelios-geofences",
sourceIdPrefix: "gelios-zone-",
sourceKind: "live_api",
sourceRevision: "gelios-rest-v1",
}),
});
/**
@@ -24,6 +43,8 @@ export function normalizeZoneSourceGeneration(value, {
}
const adapter = ZONE_SOURCE_ADAPTERS[value.adapterId];
if (!adapter) throw zoneSourceError("zone_source_adapter_unsupported");
if (value.authorityId !== adapter.authorityId) throw zoneSourceError("zone_source_authority_mismatch");
if (value.datasetId !== adapter.datasetId) throw zoneSourceError("zone_source_dataset_mismatch");
if (!isPlainObject(identityCrosswalk)) throw zoneSourceError("zone_source_identity_crosswalk_invalid");
if (value.complete !== true) throw zoneSourceError("zone_source_generation_incomplete");
const generatedAt = iso(value.generatedAt, "zone_source_generated_at_invalid");
@@ -38,7 +59,7 @@ export function normalizeZoneSourceGeneration(value, {
const facts = [];
const sourceIds = new Set();
for (const [index, rawZone] of value.zones.entries()) {
const zone = normalizeRawZone(rawZone, index, identityCrosswalk, circleSegments, value.adapterId);
const zone = normalizeRawZone(rawZone, index, identityCrosswalk, circleSegments, value.adapterId, adapter);
if (sourceIds.has(zone.sourceId)) throw zoneSourceError("zone_source_identity_duplicate");
sourceIds.add(zone.sourceId);
facts.push({
@@ -47,15 +68,29 @@ export function normalizeZoneSourceGeneration(value, {
observedAt: generatedAt,
geometry: zone.geometry,
attributes: compact({
area_square_meters: finiteNonNegative(first(rawZone.surfaceArea, rawZone.surface_area)),
area_square_meters: finiteNonNegative(first(rawZone.areaSquareMeters, rawZone.surfaceArea, rawZone.surface_area, rawZone.area)),
description: optionalString(first(rawZone.description, rawZone.descr)),
display_name: requiredString(first(rawZone.name, rawZone.display_name), "zone_source_name_required"),
display_name: requiredString(first(rawZone.displayName, rawZone.name, rawZone.display_name), "zone_source_name_required"),
applies_to_couriers: optionalBoolean(first(rawZone.appliesToCouriers, rawZone.courier)),
applies_to_kicksharing: optionalBoolean(first(rawZone.appliesToKicksharing, rawZone.kick_sharing)),
dataset_authority: adapter.authorityId,
dataset_id: adapter.datasetId,
geometry_kind: zone.geometryKind,
max_speed_kph: finiteNonNegative(first(rawZone.maxPermissibleSpeed, rawZone.max_permissible_speed)),
max_speed_kph: finiteNonNegative(first(
rawZone.maxSpeedKph,
rawZone.max_speed_kph,
rawZone.maxPermissibleSpeed,
rawZone.max_permissible_speed,
)),
perimeter_meters: finiteNonNegative(rawZone.perimeter),
schedule_timezone: optionalString(first(rawZone.scheduleTimezone, value.timezone)),
source_adapter: value.adapterId,
source_created_at: optionalString(first(rawZone.sourceCreatedAt, rawZone.create_dttm)),
source_kind: adapter.sourceKind,
source_revision: revision(value, adapter),
source_updated_at: optionalString(first(rawZone.sourceUpdatedAt, rawZone.update_dttm)),
style_color: optionalString(first(rawZone.color, rawZone.style_color)),
weekly_speed_schedule: normalizeWeeklySchedule(rawZone.weeklySpeedSchedule),
}),
});
}
@@ -78,12 +113,12 @@ export function normalizeZoneSourceGeneration(value, {
});
}
function normalizeRawZone(value, index, identityCrosswalk, circleSegments, adapterId) {
function normalizeRawZone(value, index, identityCrosswalk, circleSegments, adapterId, adapter) {
if (!isPlainObject(value)) throw zoneSourceError(`zone_source_zone_${index}_invalid`);
const nativeId = resolveNativeId(value, identityCrosswalk, adapterId);
const sourceId = nativeId.startsWith("gelios-zone-") ? nativeId : `gelios-zone-${nativeId}`;
const sourceId = nativeId.startsWith(adapter.sourceIdPrefix) ? nativeId : `${adapter.sourceIdPrefix}${nativeId}`;
if (!/^[a-z][a-z0-9._:-]{2,127}$/.test(sourceId)) throw zoneSourceError("zone_source_identity_invalid");
const rawType = String(first(value.type, value.geometry_kind, "polygon")).trim().toLowerCase();
const rawType = String(first(value.type, value.geometry?.type, value.geometry_kind, "polygon")).trim().toLowerCase();
const geometryKind = rawType === "circle" ? "circle" : rawType === "line" || rawType === "corridor" ? "corridor" : "polygon";
const geometry = normalizeGeometry(value, geometryKind, circleSegments);
if (!isBoundedGeoJsonGeometry(geometry, new Set(["Polygon", "MultiPolygon"]))) {
@@ -93,19 +128,23 @@ function normalizeRawZone(value, index, identityCrosswalk, circleSegments, adapt
}
function resolveNativeId(zone, identityCrosswalk, adapterId) {
if (adapterId === "mmap-snapshot-v1") {
const snapshotKey = optionalString(first(zone.snapshotKey, zone.snapshot_key));
if (!snapshotKey || !Object.hasOwn(identityCrosswalk, snapshotKey)) {
throw zoneSourceError("zone_source_identity_crosswalk_required");
}
const mapped = identityCrosswalk[snapshotKey];
if (mapped === undefined || mapped === null || !String(mapped).trim()) {
throw zoneSourceError("zone_source_identity_crosswalk_required");
}
return String(mapped).trim();
}
const direct = first(zone.id, zone.geozoneId, zone.zone_id, zone.sourceId);
const direct = first(
zone.nativeId,
zone.pmd_slow_zone_id,
zone.id,
zone.geozoneId,
zone.zone_id,
zone.sourceId,
);
if (direct !== undefined && direct !== null && String(direct).trim()) return String(direct).trim();
if (adapterId === "depttrans-mmap-snapshot-v1" || adapterId === "depttrans-api-snapshot-v1") {
const snapshotKey = optionalString(first(zone.snapshotKey, zone.snapshot_key));
if (snapshotKey && Object.hasOwn(identityCrosswalk, snapshotKey)) {
const mapped = identityCrosswalk[snapshotKey];
if (mapped !== undefined && mapped !== null && String(mapped).trim()) return String(mapped).trim();
}
throw zoneSourceError("zone_source_identity_crosswalk_required");
}
throw zoneSourceError("zone_source_native_identity_required");
}
@@ -201,8 +240,8 @@ function corridorPolygon(points, widthMeters) {
function revision(value, adapter) {
const explicit = optionalString(value.sourceRevision);
if (value.adapterId === "mmap-snapshot-v1") {
const digest = optionalString(value.snapshotDigest);
if (adapter.sourceKind === "versioned_snapshot") {
const digest = optionalString(first(value.contentDigest, value.snapshotDigest));
if (!digest || !/^[a-f0-9]{64}$/.test(digest)) throw zoneSourceError("zone_source_snapshot_digest_required");
return `${explicit || adapter.sourceRevision}@sha256:${digest}`;
}
@@ -232,6 +271,30 @@ function finiteNonNegative(value) {
return Number.isFinite(number) && number >= 0 ? number : undefined;
}
function optionalBoolean(value) {
return typeof value === "boolean" ? value : undefined;
}
function normalizeWeeklySchedule(value) {
if (value === undefined || value === null) return undefined;
if (!Array.isArray(value)) throw zoneSourceError("zone_source_weekly_schedule_invalid");
const entries = value.map((entry) => {
if (!isPlainObject(entry)) throw zoneSourceError("zone_source_weekly_schedule_invalid");
const day = Number(entry.dayOfWeek);
const speed = Number(entry.speedLimitKph);
const start = optionalString(entry.startTime);
const end = optionalString(entry.endTime);
if (!Number.isInteger(day) || day < 1 || day > 7
|| !Number.isFinite(speed) || speed < 0 || speed > 200
|| !/^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/.test(start || "")
|| !/^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/.test(end || "")) {
throw zoneSourceError("zone_source_weekly_schedule_invalid");
}
return `${day}@${start}-${end}=${speed}`;
});
return [...new Set(entries)].sort();
}
function requiredString(value, code) {
const result = optionalString(value);
if (!result) throw zoneSourceError(code);
@@ -44,20 +44,25 @@ assert.deepEqual(validateProviderPackage(geliosProviderPackageV4), { ok: true, e
assert.deepEqual(validateProviderPackage(geliosProviderPackageV5), { ok: true, errors: [] });
assert.deepEqual(validateProviderPackage(geliosProviderPackageV6), { ok: true, errors: [] });
const zonesProduct = geliosProviderPackageV6.dataProducts.find((product) => product.id === "map.zones.current.v1");
const zonesProduct = geliosProviderPackageV6.dataProducts.find((product) => product.id === "map.gelios-geofences.current.v1");
const registeredZonesProduct = JSON.parse(await readFile(new URL(
"../../../services/external-data-plane/definitions/map.zones.current.v1.json",
import.meta.url,
), "utf8"));
assert.deepEqual(zonesProduct, registeredZonesProduct);
assert.deepEqual(zonesProduct.semanticTypes, ["map.zone"]);
assert.equal(zonesProduct.fieldContracts.geometry.type, "geometry");
assert.equal(registeredZonesProduct.version, "1.1.0");
assert.deepEqual(registeredZonesProduct.fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]);
assert.deepEqual(registeredZonesProduct.fieldContracts.source_adapter.enum, [
"depttrans-mmap-snapshot-v1",
"depttrans-api-snapshot-v1",
]);
const zonesCapability = geliosProviderPackageV6.capabilities.find((capability) => capability.id === "gelios.geozones.current.read");
assert.equal(zonesCapability.request.query.pl, 100);
assert.equal(zonesCapability.request.query.po, 0);
assert.equal(zonesCapability.request.response.pagination.maxItems, 5000);
assert.equal(zonesCapability.request.response.pagination.totalPath, "paginationMetadata.totalCount");
const zonesMapping = geliosProviderPackageV6.mappingContracts.find((mapping) => mapping.id === "gelios.geozones.to.map.zones.current.v1");
const zonesMapping = geliosProviderPackageV6.mappingContracts.find((mapping) => mapping.id === "gelios.geozones.to.map.gelios-geofences.current.v1");
assert.deepEqual(zonesMapping.fact.observedAt.paths, ["updatedAt"]);
assert.equal(zonesMapping.fact.observedAt.fallback, "collection_received_at");
@@ -1,95 +1,157 @@
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import {
ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
normalizeZoneSourceGeneration,
validateDataProductPublish,
} from "../src/index.mjs";
import { loadZoneSourceProfile } from "../../../services/map-gateway/src/zone-source-snapshot.mjs";
const generatedAt = "2026-07-20T18:00:00.000Z";
const live = normalizeZoneSourceGeneration({
const generatedAt = "2025-06-03T11:05:00.000Z";
const snapshotDigest = "a".repeat(64);
const snapshot = normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
adapterId: "gelios-rest-v1",
sourceRevision: "gelios-rest-v1",
profileId: "moscow-pmd-slow-zones",
authorityId: "moscow-department-of-transport",
datasetId: "pmd-slow-zones",
adapterId: "depttrans-mmap-snapshot-v1",
sourceRevision: "pmd-slow-zones-202506031405",
contentDigest: snapshotDigest,
generatedAt,
timezone: "Europe/Moscow",
complete: true,
zones: [{
id: 42,
name: "Polygon zone",
nativeId: "1478",
displayName: "Петровский парк_6 (15 км/ч)",
geometry: {
type: "Polygon",
coordinates: [[
[37.60, 55.74],
[37.62, 55.74],
[37.62, 55.76],
[37.60, 55.74],
]],
},
areaSquareMeters: 75689,
appliesToKicksharing: false,
appliesToCouriers: true,
maxSpeedKph: 15,
weeklySpeedSchedule: [{
dayOfWeek: 1,
startTime: "00:00:00",
endTime: "23:59:00",
speedLimitKph: 15,
}],
}],
});
assert.equal(snapshot.complete, true);
assert.equal(snapshot.sourceKind, "versioned_snapshot");
assert.equal(snapshot.facts.length, 1);
assert.equal(snapshot.facts[0].sourceId, "depttrans-pmd-slow-zone-1478");
assert.equal(snapshot.facts[0].observedAt, generatedAt);
assert.equal(snapshot.facts[0].geometry.type, "Polygon");
assert.equal(snapshot.facts[0].attributes.dataset_authority, "moscow-department-of-transport");
assert.equal(snapshot.facts[0].attributes.dataset_id, "pmd-slow-zones");
assert.equal(snapshot.facts[0].attributes.source_adapter, "depttrans-mmap-snapshot-v1");
assert.deepEqual(snapshot.facts[0].attributes.weekly_speed_schedule, ["1@00:00:00-23:59:00=15"]);
assert.match(snapshot.sourceRevision, new RegExp(`@sha256:${snapshotDigest}$`));
assert.equal(validateDataProductPublish({
schemaVersion: "nodedc.data-product.publish/v1",
batch: {
runId: "zones-snapshot-1",
sequence: 0,
idempotencyKey: "zones-snapshot-1.batch-0",
mode: "replace",
generationAt: generatedAt,
},
facts: snapshot.facts,
}).ok, true);
const apiSnapshot = normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
authorityId: "moscow-department-of-transport",
datasetId: "pmd-slow-zones",
adapterId: "depttrans-api-snapshot-v1",
sourceRevision: "pmd-slow-zones-20260721",
contentDigest: "b".repeat(64),
generatedAt: "2026-07-21T09:00:00.000Z",
timezone: "Europe/Moscow",
complete: true,
zones: [{
nativeId: "1478",
displayName: "Петровский парк_6 (15 км/ч)",
geometry: snapshot.facts[0].geometry,
}],
});
assert.equal(apiSnapshot.facts[0].sourceId, snapshot.facts[0].sourceId);
assert.equal(apiSnapshot.facts[0].attributes.source_adapter, "depttrans-api-snapshot-v1");
const gelios = normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
authorityId: "gelios-account",
datasetId: "gelios-geofences",
adapterId: "gelios-rest-v1",
sourceRevision: "gelios-rest-v1",
generatedAt: "2026-07-21T09:00:00.000Z",
complete: true,
zones: [{
id: 1478,
name: "Account geofence",
type: "polygon",
points: [
{ latitude: 55.74, longitude: 37.60 },
{ latitude: 55.74, longitude: 37.62 },
{ latitude: 55.76, longitude: 37.62 },
],
color: "#ff00ff",
}, {
id: 43,
name: "Circle zone",
type: "circle",
points: [{ latitude: 55.75, longitude: 37.61 }],
radius: 150,
}],
});
assert.equal(live.complete, true);
assert.equal(live.sourceKind, "live_api");
assert.equal(live.facts.length, 2);
assert.deepEqual(live.facts.map((fact) => fact.sourceId), ["gelios-zone-42", "gelios-zone-43"]);
assert.equal(live.facts.every((fact) => fact.observedAt === generatedAt), true);
assert.equal(live.facts.every((fact) => fact.geometry.type === "Polygon"), true);
assert.equal(validateDataProductPublish({
schemaVersion: "nodedc.data-product.publish/v1",
batch: { runId: "zones-live-1", sequence: 0, idempotencyKey: "zones-live-1.batch-0", mode: "replace", generationAt: generatedAt },
facts: live.facts,
}).ok, true);
const snapshotDigest = "a".repeat(64);
const snapshot = normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
adapterId: "mmap-snapshot-v1",
snapshotDigest,
generatedAt,
complete: true,
zones: [{
snapshotKey: "legacy-zone-a",
name: "Snapshot zone",
type: "polygon",
points: "55.74,37.60;55.74,37.62;55.76,37.62",
}],
}, { identityCrosswalk: { "legacy-zone-a": 42 } });
assert.equal(snapshot.sourceKind, "versioned_snapshot");
assert.equal(snapshot.facts[0].sourceId, live.facts[0].sourceId);
assert.match(snapshot.sourceRevision, new RegExp(`@sha256:${snapshotDigest}$`));
assert.equal(gelios.facts[0].sourceId, "gelios-zone-1478");
assert.notEqual(gelios.facts[0].sourceId, snapshot.facts[0].sourceId);
assert.throws(() => normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
adapterId: "mmap-snapshot-v1",
snapshotDigest,
authorityId: "gelios-account",
datasetId: "pmd-slow-zones",
adapterId: "depttrans-mmap-snapshot-v1",
contentDigest: snapshotDigest,
generatedAt,
complete: true,
zones: [{ snapshotKey: "unknown", name: "Unknown", type: "polygon", points: "55.74,37.60;55.74,37.62;55.76,37.62" }],
zones: [],
}), /zone_source_authority_mismatch/);
assert.throws(() => normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
authorityId: "moscow-department-of-transport",
datasetId: "pmd-slow-zones",
adapterId: "depttrans-mmap-snapshot-v1",
contentDigest: snapshotDigest,
generatedAt,
complete: true,
zones: [{ snapshotKey: "unknown", displayName: "Unknown", geometry: snapshot.facts[0].geometry }],
}), /zone_source_identity_crosswalk_required/);
assert.throws(() => normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
adapterId: "mmap-snapshot-v1",
snapshotDigest,
generatedAt,
complete: true,
zones: [{ id: 42, name: "Uncrossed", type: "polygon", points: "55.74,37.60;55.74,37.62;55.76,37.62" }],
}), /zone_source_identity_crosswalk_required/);
assert.throws(() => normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
adapterId: "gelios-rest-v1",
generatedAt,
complete: true,
zones: [{ name: "No native id", type: "polygon", points: "55.74,37.60;55.74,37.62;55.76,37.62" }],
}), /zone_source_native_identity_required/);
assert.throws(() => normalizeZoneSourceGeneration({
schemaVersion: ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
adapterId: "gelios-rest-v1",
authorityId: "moscow-department-of-transport",
datasetId: "pmd-slow-zones",
adapterId: "depttrans-mmap-snapshot-v1",
contentDigest: snapshotDigest,
generatedAt,
complete: false,
zones: [],
}), /zone_source_generation_incomplete/);
const actualSource = await loadZoneSourceProfile(
fileURLToPath(new URL("../../../services/map-gateway/zone-sources", import.meta.url)),
"moscow-pmd-slow-zones",
);
const actual = normalizeZoneSourceGeneration(actualSource.generation);
assert.equal(actual.facts.length, 903);
assert.equal(actual.facts[0].sourceId.startsWith("depttrans-pmd-slow-zone-"), true);
assert.equal(actual.facts.every((fact) => fact.attributes.dataset_authority === "moscow-department-of-transport"), true);
assert.equal(actual.facts.every((fact) => fact.attributes.dataset_id === "pmd-slow-zones"), true);
assert.equal(actual.facts.every((fact) => fact.attributes.source_adapter === "depttrans-mmap-snapshot-v1"), true);
assert.equal(actual.facts.every((fact) => fact.attributes.weekly_speed_schedule.length === 7), true);
assert.equal(actual.facts.every((fact) => Number(fact.attributes.area_square_meters) > 0), true);
console.log("external-provider zone source: ok");