feat(provider): catalogue complete Gelios data surface

This commit is contained in:
Codex 2026-07-22 19:15:12 +03:00
parent fdda153595
commit 92070f21e8
7 changed files with 597 additions and 1 deletions

View File

@ -9,6 +9,6 @@
"./providers/*": "./providers/*/index.mjs" "./providers/*": "./providers/*/index.mjs"
}, },
"scripts": { "scripts": {
"check": "node test/contract.test.mjs && node test/data-product.test.mjs && node test/provider-package.test.mjs && node test/zone-source.test.mjs && node test/engine-private-extension.test.mjs" "check": "node test/contract.test.mjs && node test/data-product.test.mjs && node test/provider-package.test.mjs && node test/provider-capability-catalog.test.mjs && node test/zone-source.test.mjs && node test/engine-private-extension.test.mjs"
} }
} }

View File

@ -0,0 +1,28 @@
# Gelios capability catalog
`capability-catalog-v1.mjs` is the complete classified inventory for the
current `UnitModelOutput` surface and the adjacent object APIs that NODE.DC may
consume. Its authority is the official Gelios OpenAPI, checked on 2026-07-22.
The catalog is deliberately separate from an active L2 package. `implemented`
means a capability already has a governed collection and Data Product path;
`catalogued` means NODE.DC understands the surface but has not activated it;
`disabled` is mandatory for control and destructive operations.
The catalog accounts for every top-level Unit field and all twelve official
`/api/v1/units` include flags. It also records the separate read surfaces for
sensor metadata, latest sensor values, maintenance, filling, schedules and
command audit. Raw messages, decrypt material and command execution never
enter an operational UI projection.
Cadence is a product boundary:
- `hot`: current position, state, counters and classified latest readings;
- `warm`: slowly changing operational records such as maintenance plans;
- `cold`: unit, equipment and calculation profiles;
- `on_demand`: restricted or historical details that need explicit policy;
- `never`: secret, raw or command material outside the telemetry plane.
Provider packages may implement catalogued families incrementally, but they
must not move fields between classifications or cadences implicitly. A new
Data Product and its Foundry subject aspect must be explicit and versioned.

View File

@ -0,0 +1,332 @@
import { PROVIDER_CAPABILITY_CATALOG_SCHEMA_VERSION } from "../../src/provider-capability-catalog.mjs";
const unitCapabilityId = "gelios.units.current.read";
export const geliosCapabilityCatalogV1 = deepFreeze({
schemaVersion: PROVIDER_CAPABILITY_CATALOG_SCHEMA_VERSION,
providerId: "gelios",
version: "1.0.0",
authority: {
kind: "official-openapi",
url: "https://api.geliospro.com/openapi.json",
checkedAt: "2026-07-22T15:30:00Z",
},
coverage: {
schemaId: "UnitModelOutput",
sourceRoots: [
"activatedAt", "availableToUsers", "blockTime", "commands", "counters", "createdAt",
"creator", "currentUserAccess", "customFields", "driver", "extraInfo", "fuelParams",
"hwDecryptKey", "hwManufacturer", "hwType", "id", "imei", "imeiIsVisible", "isBlock",
"isFree", "lastMsg", "lastSensorsVal", "maintenancePlan", "name", "phone", "phone2",
"phone2IsVisible", "phoneIsVisible", "preSetCommandGroup", "removed", "removedAt", "sensors",
"stationaryLat", "stationaryLon", "storageInfo", "tripParams", "unblockTime", "unitType",
],
},
queryFlags: [
flag("inclextra", ["gelios.unit.asset_profile", "gelios.unit.asset_identifiers", "gelios.unit.extra_internal"]),
flag("incltrip", ["gelios.unit.trip_settings"]),
flag("inclfuel", ["gelios.unit.fuel_settings"]),
flag("inclcntrs", ["gelios.unit.counters"]),
flag("inclcstmflds", ["gelios.unit.custom_fields"]),
flag("inclsnsrs", ["gelios.unit.sensor_definitions", "gelios.unit.sensor_conversion"]),
flag("inclcmmnds", ["gelios.unit.command_catalog"]),
flag("incldrvrs", ["gelios.unit.driver"]),
flag("inclstinf", ["gelios.unit.storage"]),
flag("inclusersav", ["gelios.unit.access_metadata"]),
flag("incllsv", ["gelios.unit.latest_sensor_values"]),
flag("inclplan", ["gelios.unit.maintenance_plan"]),
],
dataFamilies: [
family({
id: "gelios.unit.identity",
title: "Stable unit identity and type",
sourcePaths: ["id", "name", "unitType.id", "unitType.name", "unitType.type"],
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.unit"], uiTab: "overview",
}),
family({
id: "gelios.unit.lifecycle",
title: "Activation, blocking and removal lifecycle",
sourcePaths: ["activatedAt", "blockTime", "createdAt", "isBlock", "isFree", "removed", "removedAt", "unblockTime"],
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.unit"], uiTab: "overview",
}),
family({
id: "gelios.unit.access_metadata",
title: "Provider ownership and access metadata",
sourcePaths: ["availableToUsers[]", "creator.id", "creator.login", "currentUserAccess"],
cadence: "cold", classification: "internal", disposition: "catalogue", status: "catalogued",
ontologyEntities: ["gelios.unit"],
}),
family({
id: "gelios.unit.equipment",
title: "Tracker hardware classification",
sourcePaths: ["hwManufacturer.id", "hwManufacturer.name", "hwType.id", "hwType.name"],
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.tracker_device"], uiTab: "equipment",
}),
family({
id: "gelios.unit.device_identifiers",
title: "Restricted device and phone identifiers",
sourcePaths: ["imei", "imeiIsVisible", "phone", "phone2", "phone2IsVisible", "phoneIsVisible"],
cadence: "on_demand", classification: "restricted", disposition: "profile", status: "catalogued",
ontologyEntities: ["gelios.tracker_device"], uiTab: "equipment",
}),
family({
id: "gelios.unit.cryptographic_material",
title: "Provider cryptographic material",
sourcePaths: ["hwDecryptKey"],
cadence: "never", classification: "secret", disposition: "forbid", status: "forbidden",
ontologyEntities: ["gelios.tracker_device"],
}),
family({
id: "gelios.unit.stationary_location",
title: "Restricted configured stationary location",
sourcePaths: ["stationaryLat", "stationaryLon"],
cadence: "on_demand", classification: "restricted", disposition: "profile", status: "catalogued",
ontologyEntities: ["gelios.unit"], uiTab: "position",
}),
family({
id: "gelios.unit.position_current",
title: "Current normalized position and movement sample",
sourcePaths: [
"lastMsg.course", "lastMsg.height", "lastMsg.lat", "lastMsg.lon", "lastMsg.receiveTime",
"lastMsg.sats", "lastMsg.speed", "lastMsg.time",
],
cadence: "hot", classification: "operational", disposition: "publish", status: "implemented",
ontologyEntities: ["gelios.telemetry_snapshot", "map.moving_object"], uiTab: "position",
dataProductIds: ["fleet.positions.current.v5"],
}),
family({
id: "gelios.unit.raw_message_parameters",
title: "Unclassified raw tracker message parameters",
sourcePaths: ["lastMsg.params"],
cadence: "never", classification: "unclassified", disposition: "forbid", status: "forbidden",
ontologyEntities: ["gelios.telemetry_snapshot"],
}),
family({
id: "gelios.unit.counters",
title: "Operational mileage and engine-hour counters",
sourcePaths: ["counters.engineHours.value", "counters.mileage.value"],
cadence: "hot", classification: "operational", disposition: "publish", status: "implemented",
ontologyEntities: ["gelios.telemetry_snapshot"], uiTab: "counters",
dataProductIds: ["fleet.positions.current.v5"],
}),
family({
id: "gelios.unit.latest_sensor_values",
title: "Latest values for explicitly classified sensors",
sourcePaths: ["lastSensorsVal[].tm", "lastSensorsVal[].val"],
cadence: "hot", classification: "unclassified", disposition: "profile", status: "implemented",
ontologyEntities: ["gelios.sensor_reading"], uiTab: "telemetry",
}),
family({
id: "gelios.unit.sensor_definitions",
title: "Sensor identity, units, visibility and limits",
sourcePaths: [
"sensors[].displayFuelTankVolume", "sensors[].fuelConsumptionRate", "sensors[].fuelTankVolume",
"sensors[].id", "sensors[].isVisible", "sensors[].maxValidParamValue", "sensors[].measure",
"sensors[].minValidParamValue", "sensors[].msgParam", "sensors[].name", "sensors[].type.id", "sensors[].type.name",
],
cadence: "cold", classification: "unclassified", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.sensor_definition"], uiTab: "equipment",
}),
family({
id: "gelios.unit.sensor_conversion",
title: "Raw sensor conversion tables",
sourcePaths: ["sensors[].conversionTable"],
cadence: "cold", classification: "internal", disposition: "catalogue", status: "catalogued",
ontologyEntities: ["gelios.sensor_definition"],
}),
family({
id: "gelios.unit.asset_profile",
title: "Vehicle and asset descriptive profile",
sourcePaths: [
"extraInfo.brand", "extraInfo.color", "extraInfo.engineCubicCapacity", "extraInfo.enginePower",
"extraInfo.fuelType", "extraInfo.iconLink", "extraInfo.model", "extraInfo.year",
],
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.unit"], uiTab: "equipment",
}),
family({
id: "gelios.unit.asset_identifiers",
title: "Restricted VIN and number plate",
sourcePaths: ["extraInfo.numberPlate", "extraInfo.vin"],
cadence: "on_demand", classification: "restricted", disposition: "profile", status: "catalogued",
ontologyEntities: ["gelios.unit"], uiTab: "equipment",
}),
family({
id: "gelios.unit.extra_internal",
title: "Provider autocomplete metadata",
sourcePaths: ["extraInfo.autocompleteParams"],
cadence: "cold", classification: "internal", disposition: "catalogue", status: "catalogued",
ontologyEntities: ["gelios.unit"],
}),
family({
id: "gelios.unit.trip_settings",
title: "Trip detection and movement policy",
sourcePaths: prefixed("tripParams", [
"correctedEngineHoursFactor", "correctedMileageFactor", "filterBySatellitesCountIsOn",
"filterBySatellitesCountValue", "filterEmissions", "limitAcceleration", "lostConnectionIsOn",
"lostConnectionTimeValue", "maximumPermissibleSpeed", "maximumValidHeight", "maximumValidSpeed",
"mileageByIgnition", "minimumMovementSpeed", "minimumMovementTime", "minimumParkingTime",
"minimumTimeStop", "minimumTripDistance", "minimumValidHeight", "speedMeasurementItemsId",
"speedParam", "tripDetectionType.id", "tripDetectionType.name", "useOdometer",
]),
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.unit"], uiTab: "settings",
}),
family({
id: "gelios.unit.fuel_settings",
title: "Fuel calculation and theft/filling policy",
sourcePaths: prefixed("fuelParams", [
"cityConsumptionSummer", "cityConsumptionWinter", "fillingByIgnOff", "fillingInterval",
"fillingSplitTimeout", "fillingTheftOnParkingOnly", "filterZeroFls", "idleConsumptionSummer",
"idleConsumptionWinter", "ignoreMsgsTimeAfterStart", "includeTheftsInConsumption",
"maximumConsumptionForPeriod", "minimumFilling", "minimumFuelTheft", "outOfTheCityConsumptionSummer",
"outOfTheCityConsumptionWinter", "secondsToIgnoreAfterMovementStart", "theftByIgnOff",
"theftSplitTimeout", "useAverageFlsValue", "useFilterIsOn", "useFilterValue",
"useNewFuelCalcAlgorithm", "useSeparateFls", "winterEnd", "winterStart",
]),
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.fuel_profile"], uiTab: "settings",
}),
family({
id: "gelios.unit.maintenance_plan",
title: "Maintenance thresholds and progress",
sourcePaths: prefixed("maintenancePlan[]", [
"created_at", "description", "id", "interval_day", "interval_mileage", "interval_motohour",
"is_day", "is_mileage", "is_motohour", "last_day", "last_mileage", "last_motohour",
"name", "repeat_count", "total_count", "unit_id", "updated_at",
]),
cadence: "warm", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.maintenance_plan"], uiTab: "maintenance",
}),
family({
id: "gelios.unit.custom_fields",
title: "Tenant-defined fields requiring explicit classification",
sourcePaths: ["customFields[].id", "customFields[].isVisible", "customFields[].name", "customFields[].value"],
cadence: "cold", classification: "unclassified", disposition: "profile", status: "catalogued",
ontologyEntities: ["gelios.custom_field"], uiTab: "diagnostics",
}),
family({
id: "gelios.unit.driver",
title: "Driver personal data",
sourcePaths: ["driver.name", "driver.phone"],
cadence: "on_demand", classification: "restricted", disposition: "profile", status: "catalogued",
ontologyEntities: ["gelios.unit"],
}),
family({
id: "gelios.unit.storage",
title: "Provider storage policy",
sourcePaths: [
"storageInfo.storagePeriod.description", "storageInfo.storagePeriod.id", "storageInfo.storagePeriod.name",
"storageInfo.storagePeriodLastUpdUtc",
],
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.unit"], uiTab: "equipment",
}),
family({
id: "gelios.unit.command_catalog",
title: "Command templates and preset groups",
sourcePaths: [
"commands[].id", "commands[].name", "commands[].text", "commands[].type",
"preSetCommandGroup.id", "preSetCommandGroup.name",
],
cadence: "never", classification: "control", disposition: "catalogue", status: "catalogued",
ontologyEntities: ["integration.command_capability"],
}),
family({
id: "gelios.unit.maintenance_events",
title: "Maintenance event history",
sourcePaths: ["maintenanceEvents[].id", "maintenanceEvents[].date", "maintenanceEvents[].description"],
cadence: "on_demand", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.maintenance_plan"], uiTab: "maintenance",
}),
family({
id: "gelios.unit.filling_events",
title: "Fuel filling event history",
sourcePaths: ["fillingEvents[].id", "fillingEvents[].date", "fillingEvents[].volume", "fillingEvents[].price"],
cadence: "on_demand", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.fuel_profile"], uiTab: "maintenance",
}),
family({
id: "gelios.unit.work_schedule",
title: "Shifts and work-time rules",
sourcePaths: ["shifts[].id", "shifts[].name", "workTime[].id", "workTime[].startDay", "workTime[].endDay"],
cadence: "cold", classification: "operational", disposition: "profile", status: "planned",
ontologyEntities: ["gelios.unit"], uiTab: "settings",
}),
family({
id: "gelios.unit.command_history",
title: "Command execution audit history",
sourcePaths: ["commandHistory[].id", "commandHistory[].status", "commandHistory[].timeCmd", "commandHistory[].timeSend"],
cadence: "on_demand", classification: "control", disposition: "catalogue", status: "catalogued",
ontologyEntities: ["integration.command_capability"],
}),
family({
id: "gelios.unit.raw_messages",
title: "Pure and raw tracker messages",
sourcePaths: ["rawMessages[].payload"],
cadence: "never", classification: "restricted", disposition: "forbid", status: "forbidden",
ontologyEntities: ["gelios.telemetry_snapshot"],
}),
family({
id: "gelios.geozones.current",
title: "Provider geozone catalogue",
sourcePaths: ["geozones[].geometry", "geozones[].id", "geozones[].name"],
cadence: "cold", classification: "operational", disposition: "publish", status: "implemented",
ontologyEntities: ["map.zone"], dataProductIds: ["map.gelios-geofences.current.v1"],
}),
],
capabilities: [
capability("gelios.units.current.read", "GET", "/api/v1/units", "read", "implemented", [
"gelios.unit.identity", "gelios.unit.lifecycle", "gelios.unit.access_metadata", "gelios.unit.equipment",
"gelios.unit.device_identifiers", "gelios.unit.cryptographic_material", "gelios.unit.stationary_location",
"gelios.unit.position_current", "gelios.unit.raw_message_parameters", "gelios.unit.counters",
"gelios.unit.latest_sensor_values", "gelios.unit.sensor_definitions", "gelios.unit.sensor_conversion",
"gelios.unit.asset_profile", "gelios.unit.asset_identifiers", "gelios.unit.extra_internal",
"gelios.unit.trip_settings", "gelios.unit.fuel_settings", "gelios.unit.maintenance_plan",
"gelios.unit.custom_fields", "gelios.unit.driver", "gelios.unit.storage", "gelios.unit.command_catalog",
]),
capability("gelios.unit.current.read", "GET", "/api/v1/units/{unit_id}", "read", "catalogued", ["gelios.unit.identity"]),
capability("gelios.units.last_messages.read", "POST", "/api/v1/units/last-msgs/full", "read", "catalogued", ["gelios.unit.position_current"]),
capability("gelios.unit.sensors.read", "GET", "/api/v1/units/{unit_id}/sensors", "metadata", "catalogued", ["gelios.unit.sensor_definitions", "gelios.unit.sensor_conversion"]),
capability("gelios.unit.sensor_values.read", "GET", "/api/v1/units/{unit_id}/sensors/values/last", "read", "catalogued", ["gelios.unit.latest_sensor_values"]),
capability("gelios.unit.maintenance_plan.read", "GET", "/api/v1/units/{unit_id}/maintenance/plan", "read", "catalogued", ["gelios.unit.maintenance_plan"]),
capability("gelios.unit.maintenance_events.read", "GET", "/api/v1/units/{unit_id}/maintenance/event", "read", "catalogued", ["gelios.unit.maintenance_events"]),
capability("gelios.unit.filling_events.read", "GET", "/api/v1/units/{unit_id}/filling/event", "read", "catalogued", ["gelios.unit.filling_events"]),
capability("gelios.unit.shifts.read", "GET", "/api/v1/units/{unit_id}/shifts", "read", "catalogued", ["gelios.unit.work_schedule"]),
capability("gelios.unit.work_time.read", "GET", "/api/v1/units/{unit_id}/work-time", "read", "catalogued", ["gelios.unit.work_schedule"]),
capability("gelios.unit.command_history.read", "GET", "/api/v1/units/{unit_id}/commands/history", "metadata", "catalogued", ["gelios.unit.command_history"]),
capability("gelios.unit.commands.read", "GET", "/api/v1/units/{unit_id}/commands", "metadata", "catalogued", ["gelios.unit.command_catalog"]),
capability("gelios.unit.last_message_params.read", "GET", "/api/v1/units/{unit_id}/last-msg-params", "metadata", "catalogued", ["gelios.unit.raw_message_parameters"]),
capability("gelios.unit.raw_message.read", "GET", "/api/v1/units/{unit_id}/last-msg/raw", "read", "disabled", ["gelios.unit.raw_messages"]),
capability("gelios.geozones.current.read", "GET", "/api/v1/geozones", "read", "implemented", ["gelios.geozones.current"]),
capability("gelios.unit.command.send", "POST", "/api/v1/units/{unit_id}/commands/send", "control", "disabled", ["gelios.unit.command_catalog"]),
capability("gelios.unit.update", "PATCH", "/api/v1/units/{unit_id}", "write", "disabled", ["gelios.unit.identity"]),
capability("gelios.unit.delete", "DELETE", "/api/v1/units/{unit_id}", "destructive", "disabled", ["gelios.unit.lifecycle"]),
],
});
function flag(id, familyIds) {
return { id, capabilityId: unitCapabilityId, familyIds };
}
function family({ dataProductIds = [], ...value }) {
return { ...value, dataProductIds };
}
function capability(id, method, path, classification, status, familyIds) {
return { id, method, path, classification, status, familyIds };
}
function prefixed(prefix, fields) {
return fields.map((field) => `${prefix}.${field}`);
}
function deepFreeze(input) {
if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
Object.freeze(input);
for (const child of Object.values(input)) deepFreeze(child);
return input;
}

View File

@ -0,0 +1,2 @@
export { geliosCapabilityCatalogV1 } from "./capability-catalog-v1.mjs";
export * from "./v7/index.mjs";

View File

@ -35,6 +35,11 @@ export {
validateProviderPackage, validateProviderPackage,
} from "./provider-package.mjs"; } from "./provider-package.mjs";
export {
PROVIDER_CAPABILITY_CATALOG_SCHEMA_VERSION,
validateProviderCapabilityCatalog,
} from "./provider-capability-catalog.mjs";
export { export {
DATA_PRODUCT_HISTORY_SCHEMA_VERSION, DATA_PRODUCT_HISTORY_SCHEMA_VERSION,
DATA_PRODUCT_PATCH_SCHEMA_VERSION, DATA_PRODUCT_PATCH_SCHEMA_VERSION,

View File

@ -0,0 +1,178 @@
export const PROVIDER_CAPABILITY_CATALOG_SCHEMA_VERSION = "nodedc.external-provider-capability-catalog/v1";
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,159}$/;
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
const SOURCE_PATH = /^[A-Za-z_][A-Za-z0-9_]*(?:\[\])?(?:\.[A-Za-z_][A-Za-z0-9_]*(?:\[\])?)*$/;
const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
const CAPABILITY_CLASSES = new Set(["read", "metadata", "write", "destructive", "control"]);
const CAPABILITY_STATUSES = new Set(["implemented", "catalogued", "disabled"]);
const CADENCE_CLASSES = new Set(["hot", "warm", "cold", "on_demand", "never"]);
const DATA_CLASSES = new Set(["operational", "restricted", "secret", "control", "internal", "unclassified"]);
const DISPOSITIONS = new Set(["publish", "profile", "catalogue", "forbid"]);
const FAMILY_STATUSES = new Set(["implemented", "planned", "catalogued", "forbidden"]);
const SENSITIVE_PATH = /(?:imei|phone|decrypt|vin|numberPlate|driver|creator|access|customFields|stationary(?:Lat|Lon)|(?:^|\.)raw(?:\.|$)|(?:^|\.)params(?:\.|$))/i;
const CATALOG_KEYS = new Set(["schemaVersion", "providerId", "version", "authority", "coverage", "queryFlags", "dataFamilies", "capabilities"]);
const AUTHORITY_KEYS = new Set(["kind", "url", "checkedAt"]);
const COVERAGE_KEYS = new Set(["schemaId", "sourceRoots"]);
const QUERY_FLAG_KEYS = new Set(["id", "capabilityId", "familyIds"]);
const FAMILY_KEYS = new Set(["id", "title", "sourcePaths", "cadence", "classification", "disposition", "status", "ontologyEntities", "uiTab", "dataProductIds"]);
const CAPABILITY_KEYS = new Set(["id", "method", "path", "classification", "status", "familyIds"]);
export function validateProviderCapabilityCatalog(value) {
const errors = [];
if (!plain(value)) return result(["catalog_must_be_object"]);
rejectUnknown(value, CATALOG_KEYS, "catalog", errors);
if (value.schemaVersion !== PROVIDER_CAPABILITY_CATALOG_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
identifier(value.providerId, "providerId", errors);
if (typeof value.version !== "string" || !SEMVER.test(value.version)) errors.push("version_invalid");
validateAuthority(value.authority, errors);
validateCoverage(value.coverage, errors);
const families = artifactArray(value.dataFamilies, "dataFamilies", validateFamily, errors);
const capabilities = artifactArray(value.capabilities, "capabilities", validateCapability, errors);
const flags = artifactArray(value.queryFlags, "queryFlags", validateQueryFlag, errors);
unique(families.map((item) => item.id), "dataFamilies.id_duplicate", errors);
unique(capabilities.map((item) => item.id), "capabilities.id_duplicate", errors);
unique(flags.map((item) => item.id), "queryFlags.id_duplicate", errors);
const familyIds = new Set(families.map((item) => item.id));
const capabilityIds = new Set(capabilities.map((item) => item.id));
for (const [index, family] of families.entries()) {
for (const entity of family.ontologyEntities || []) identifier(entity, `dataFamilies[${index}].ontologyEntities`, errors);
for (const product of family.dataProductIds || []) identifier(product, `dataFamilies[${index}].dataProductIds`, errors);
if (family.disposition === "publish" && !(family.dataProductIds || []).length) {
errors.push(`dataFamilies[${index}].publish_requires_data_product`);
}
if (new Set(["restricted", "secret", "control", "internal", "unclassified"]).has(family.classification)
&& family.disposition === "publish") {
errors.push(`dataFamilies[${index}].restricted_family_must_not_publish`);
}
if ((family.sourcePaths || []).some((path) => SENSITIVE_PATH.test(path))
&& family.classification === "operational") {
errors.push(`dataFamilies[${index}].sensitive_path_must_be_classified`);
}
}
unique(families.flatMap((family) => family.sourcePaths || []), "dataFamilies.sourcePath_duplicate", errors);
for (const [index, capability] of capabilities.entries()) {
for (const familyId of capability.familyIds || []) {
if (!familyIds.has(familyId)) errors.push(`capabilities[${index}].family_not_found:${familyId}`);
}
if (capability.classification === "control" && capability.status !== "disabled") {
errors.push(`capabilities[${index}].control_capability_must_be_disabled`);
}
if (capability.classification === "destructive" && capability.status !== "disabled") {
errors.push(`capabilities[${index}].destructive_capability_must_be_disabled`);
}
}
for (const [index, flag] of flags.entries()) {
if (!capabilityIds.has(flag.capabilityId)) errors.push(`queryFlags[${index}].capability_not_found`);
for (const familyId of flag.familyIds || []) {
if (!familyIds.has(familyId)) errors.push(`queryFlags[${index}].family_not_found:${familyId}`);
}
}
const roots = new Set(families.flatMap((family) => (family.sourcePaths || []).map(sourceRoot)));
for (const root of value.coverage?.sourceRoots || []) {
if (!roots.has(root)) errors.push(`coverage.source_root_unaccounted:${root}`);
}
return result(errors);
}
function validateAuthority(value, errors) {
if (!plain(value)) return errors.push("authority_must_be_object");
rejectUnknown(value, AUTHORITY_KEYS, "authority", errors);
if (value.kind !== "official-openapi") errors.push("authority.kind_invalid");
if (typeof value.url !== "string" || !/^https:\/\//.test(value.url)) errors.push("authority.url_invalid");
if (typeof value.checkedAt !== "string" || Number.isNaN(Date.parse(value.checkedAt))) errors.push("authority.checkedAt_invalid");
}
function validateCoverage(value, errors) {
if (!plain(value)) return errors.push("coverage_must_be_object");
rejectUnknown(value, COVERAGE_KEYS, "coverage", errors);
if (typeof value.schemaId !== "string" || !value.schemaId.trim()) errors.push("coverage.schemaId_invalid");
strings(value.sourceRoots, "coverage.sourceRoots", errors, { sourcePaths: true, min: 1 });
}
function validateFamily(value, index, errors) {
const path = `dataFamilies[${index}]`;
if (!plain(value)) return errors.push(`${path}_must_be_object`);
rejectUnknown(value, FAMILY_KEYS, path, errors);
identifier(value.id, `${path}.id`, errors);
nonempty(value.title, `${path}.title`, errors);
strings(value.sourcePaths, `${path}.sourcePaths`, errors, { sourcePaths: true, min: 1 });
if (!CADENCE_CLASSES.has(value.cadence)) errors.push(`${path}.cadence_invalid`);
if (!DATA_CLASSES.has(value.classification)) errors.push(`${path}.classification_invalid`);
if (!DISPOSITIONS.has(value.disposition)) errors.push(`${path}.disposition_invalid`);
if (!FAMILY_STATUSES.has(value.status)) errors.push(`${path}.status_invalid`);
strings(value.ontologyEntities, `${path}.ontologyEntities`, errors);
strings(value.dataProductIds, `${path}.dataProductIds`, errors);
if (value.uiTab !== undefined && value.uiTab !== null) identifier(value.uiTab, `${path}.uiTab`, errors);
}
function validateCapability(value, index, errors) {
const path = `capabilities[${index}]`;
if (!plain(value)) return errors.push(`${path}_must_be_object`);
rejectUnknown(value, CAPABILITY_KEYS, path, errors);
identifier(value.id, `${path}.id`, errors);
if (!HTTP_METHODS.has(value.method)) errors.push(`${path}.method_invalid`);
if (typeof value.path !== "string" || !value.path.startsWith("/api/")) errors.push(`${path}.path_invalid`);
if (!CAPABILITY_CLASSES.has(value.classification)) errors.push(`${path}.classification_invalid`);
if (!CAPABILITY_STATUSES.has(value.status)) errors.push(`${path}.status_invalid`);
strings(value.familyIds, `${path}.familyIds`, errors);
}
function validateQueryFlag(value, index, errors) {
const path = `queryFlags[${index}]`;
if (!plain(value)) return errors.push(`${path}_must_be_object`);
rejectUnknown(value, QUERY_FLAG_KEYS, path, errors);
if (typeof value.id !== "string" || !/^[a-z][a-z0-9_]{2,63}$/.test(value.id)) errors.push(`${path}.id_invalid`);
identifier(value.capabilityId, `${path}.capabilityId`, errors);
strings(value.familyIds, `${path}.familyIds`, errors, { min: 1 });
}
function artifactArray(value, path, validator, errors) {
if (!Array.isArray(value) || value.length < 1 || value.length > 256) {
errors.push(`${path}_must_be_bounded_nonempty_array`);
return [];
}
value.forEach((item, index) => validator(item, index, errors));
return value.filter(plain);
}
function strings(value, path, errors, { sourcePaths = false, min = 0 } = {}) {
if (!Array.isArray(value) || value.length < min || value.length > 512) return errors.push(`${path}_invalid`);
for (const item of value) {
if (typeof item !== "string" || !item.trim() || (sourcePaths && !SOURCE_PATH.test(item))) errors.push(`${path}_invalid`);
}
unique(value, `${path}_duplicate`, errors);
}
function identifier(value, path, errors) {
if (typeof value !== "string" || !IDENTIFIER.test(value)) errors.push(`${path}_invalid`);
}
function nonempty(value, path, errors) {
if (typeof value !== "string" || !value.trim()) errors.push(`${path}_invalid`);
}
function rejectUnknown(value, allowed, path, errors) {
if (!plain(value)) return;
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
}
function unique(values, code, errors) {
if (new Set(values).size !== values.length) errors.push(code);
}
function sourceRoot(path) {
return String(path).split(".")[0].replace(/\[\]$/, "");
}
function plain(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function result(errors) {
return { ok: errors.length === 0, errors };
}

View File

@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import { validateProviderCapabilityCatalog } from "../src/index.mjs";
import { geliosCapabilityCatalogV1, geliosProviderPackageV7 } from "../providers/gelios/index.mjs";
assert.deepEqual(validateProviderCapabilityCatalog(geliosCapabilityCatalogV1), { ok: true, errors: [] });
const expectedUnitRoots = [
"activatedAt", "availableToUsers", "blockTime", "commands", "counters", "createdAt",
"creator", "currentUserAccess", "customFields", "driver", "extraInfo", "fuelParams",
"hwDecryptKey", "hwManufacturer", "hwType", "id", "imei", "imeiIsVisible", "isBlock",
"isFree", "lastMsg", "lastSensorsVal", "maintenancePlan", "name", "phone", "phone2",
"phone2IsVisible", "phoneIsVisible", "preSetCommandGroup", "removed", "removedAt", "sensors",
"stationaryLat", "stationaryLon", "storageInfo", "tripParams", "unblockTime", "unitType",
];
assert.deepEqual(geliosCapabilityCatalogV1.coverage.sourceRoots, expectedUnitRoots);
const expectedIncludeFlags = [
"inclextra", "incltrip", "inclfuel", "inclcntrs", "inclcstmflds", "inclsnsrs",
"inclcmmnds", "incldrvrs", "inclstinf", "inclusersav", "incllsv", "inclplan",
];
assert.deepEqual(geliosCapabilityCatalogV1.queryFlags.map(({ id }) => id), expectedIncludeFlags);
const unitCapability = geliosProviderPackageV7.capabilities.find(({ id }) => id === "gelios.units.current.read");
assert.ok(unitCapability);
for (const enabledFlag of Object.keys(unitCapability.request.query)) {
assert.ok(expectedIncludeFlags.includes(enabledFlag), `v7 query flag is missing from the capability catalog: ${enabledFlag}`);
}
assert.deepEqual(
geliosCapabilityCatalogV1.capabilities.filter(({ status }) => status === "implemented").map(({ id }) => id),
["gelios.units.current.read", "gelios.geozones.current.read"],
);
assert.equal(
geliosCapabilityCatalogV1.capabilities.filter(({ classification }) => classification === "control").every(({ status }) => status === "disabled"),
true,
);
const restrictedPublish = structuredClone(geliosCapabilityCatalogV1);
restrictedPublish.dataFamilies.find(({ id }) => id === "gelios.unit.device_identifiers").disposition = "publish";
restrictedPublish.dataFamilies.find(({ id }) => id === "gelios.unit.device_identifiers").dataProductIds = ["unsafe.identifiers.current.v1"];
assert.equal(validateProviderCapabilityCatalog(restrictedPublish).ok, false);
const missingCoverage = structuredClone(geliosCapabilityCatalogV1);
missingCoverage.dataFamilies.find(({ id }) => id === "gelios.unit.identity").sourcePaths = ["name", "unitType.id"];
assert.ok(validateProviderCapabilityCatalog(missingCoverage).errors.includes("coverage.source_root_unaccounted:id"));
const enabledControl = structuredClone(geliosCapabilityCatalogV1);
enabledControl.capabilities.find(({ id }) => id === "gelios.unit.command.send").status = "catalogued";
assert.equal(validateProviderCapabilityCatalog(enabledControl).ok, false);
console.log("Gelios provider capability catalog: ok");