From 73364b4acb47cd204eba47967a3f69049c868053 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 23 Jul 2026 23:27:31 +0300 Subject: [PATCH] feat(provider-contract): add Gelios v9 telemetry projection --- packages/external-provider-contract/README.md | 5 ++ .../providers/gelios/index.mjs | 1 + .../providers/gelios/v9/index.mjs | 6 ++ .../providers/gelios/v9/package.mjs | 22 ++++++ .../gelios/v9/telemetry-field-registry.mjs | 39 +++++++++++ .../external-provider-contract/src/index.mjs | 1 + .../src/l2-execution-plan.mjs | 10 ++- .../test/l2-execution-plan.test.mjs | 69 ++++++++++++++----- .../test/provider-package.test.mjs | 5 ++ .../test/telemetry-field-registry.test.mjs | 34 +++++++++ 10 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 packages/external-provider-contract/providers/gelios/v9/index.mjs create mode 100644 packages/external-provider-contract/providers/gelios/v9/package.mjs create mode 100644 packages/external-provider-contract/providers/gelios/v9/telemetry-field-registry.mjs diff --git a/packages/external-provider-contract/README.md b/packages/external-provider-contract/README.md index aac2128..f396482 100644 --- a/packages/external-provider-contract/README.md +++ b/packages/external-provider-contract/README.md @@ -53,6 +53,11 @@ Foundry. collection/request/extract/mapping/publish, прикладывает полные декларативные request/mapping contracts и фиксирует SHA-256 digests package, profile, template, mapping, field policy, Data Product и telemetry registry. Plan +по умолчанию компилируется версией `1.2.0`; immutable plans `1.1.0` остаются +валидируемыми для воспроизводимости уже материализованных legacy runtime. +Версия компилятора выбирает только provider-neutral runtime semantics и не +добавляет ветвление по provider ID. +Plan содержит exact `nodedc.l2-graph-blueprint/v1`: линейный набор generic runtime node kinds, exact edges, config digests, credential slots, trigger/retry/batch/ cardinality policy и закрытую матрицу требуемых mapping operators. Blueprint diff --git a/packages/external-provider-contract/providers/gelios/index.mjs b/packages/external-provider-contract/providers/gelios/index.mjs index 342dc86..fa43e40 100644 --- a/packages/external-provider-contract/providers/gelios/index.mjs +++ b/packages/external-provider-contract/providers/gelios/index.mjs @@ -2,3 +2,4 @@ export { geliosCapabilityCatalogV1 } from "./capability-catalog-v1.mjs"; export { geliosCapabilityCatalogV2 } from "./capability-catalog-v2.mjs"; export { geliosProviderPackageV7 } from "./v7/index.mjs"; export * from "./v8/index.mjs"; +export * from "./v9/index.mjs"; diff --git a/packages/external-provider-contract/providers/gelios/v9/index.mjs b/packages/external-provider-contract/providers/gelios/v9/index.mjs new file mode 100644 index 0000000..016e754 --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v9/index.mjs @@ -0,0 +1,6 @@ +export { + GELIOS_PROVIDER_PACKAGE_ID, + GELIOS_PROVIDER_PACKAGE_VERSION, + geliosProviderPackageV9, +} from "./package.mjs"; +export { geliosTelemetryFieldRegistryV2 } from "./telemetry-field-registry.mjs"; diff --git a/packages/external-provider-contract/providers/gelios/v9/package.mjs b/packages/external-provider-contract/providers/gelios/v9/package.mjs new file mode 100644 index 0000000..93eac7d --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v9/package.mjs @@ -0,0 +1,22 @@ +import { geliosProviderPackageV8 } from "../v8/package.mjs"; + +export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v9"; +export const GELIOS_PROVIDER_PACKAGE_VERSION = "9.0.0"; + +const value = structuredClone(geliosProviderPackageV8); +value.id = GELIOS_PROVIDER_PACKAGE_ID; +value.version = GELIOS_PROVIDER_PACKAGE_VERSION; +value.manifest = { + ...value.manifest, + id: "gelios.provider.manifest.v9", + version: GELIOS_PROVIDER_PACKAGE_VERSION, +}; + +export const geliosProviderPackageV9 = deepFreeze(value); + +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; +} diff --git a/packages/external-provider-contract/providers/gelios/v9/telemetry-field-registry.mjs b/packages/external-provider-contract/providers/gelios/v9/telemetry-field-registry.mjs new file mode 100644 index 0000000..dbe9ee0 --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v9/telemetry-field-registry.mjs @@ -0,0 +1,39 @@ +import { + geliosTelemetryFieldRegistryV1, +} from "../v8/telemetry-field-registry.mjs"; +import { + GELIOS_PROVIDER_PACKAGE_ID, + GELIOS_PROVIDER_PACKAGE_VERSION, +} from "./package.mjs"; + +const value = structuredClone(geliosTelemetryFieldRegistryV1); +value.id = "gelios.units.current.telemetry.v2"; +value.version = "2.0.0"; +value.providerPackage = { + id: GELIOS_PROVIDER_PACKAGE_ID, + version: GELIOS_PROVIDER_PACKAGE_VERSION, +}; +value.entries = value.entries.map((entry) => { + if (!["in_0", "in_1"].includes(entry.sourceKey)) return entry; + return { + ...entry, + status: "approved", + sensitivity: "operational", + valueType: "number", + semanticReading: { + id: `sensor.param.${entry.sourceKey}`, + labelSource: "provider_configured", + unitSource: "provider_configured", + }, + allowedSurfaces: ["audit", "data_product", "analytics", "foundry"], + }; +}); + +export const geliosTelemetryFieldRegistryV2 = deepFreeze(value); + +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; +} diff --git a/packages/external-provider-contract/src/index.mjs b/packages/external-provider-contract/src/index.mjs index 36c2054..cf25d38 100644 --- a/packages/external-provider-contract/src/index.mjs +++ b/packages/external-provider-contract/src/index.mjs @@ -23,6 +23,7 @@ export { } from "./telemetry-field-registry.mjs"; export { L2_EXECUTION_PLAN_COMPILER_VERSION, + L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, L2_EXECUTION_PLAN_SCHEMA_VERSION, L2_GRAPH_BLUEPRINT_SCHEMA_VERSION, L2_MAPPING_RUNTIME_SCHEMA_VERSION, diff --git a/packages/external-provider-contract/src/l2-execution-plan.mjs b/packages/external-provider-contract/src/l2-execution-plan.mjs index 9d2c4a6..bb0dc4a 100644 --- a/packages/external-provider-contract/src/l2-execution-plan.mjs +++ b/packages/external-provider-contract/src/l2-execution-plan.mjs @@ -9,7 +9,11 @@ export const L2_EXECUTION_PLAN_SCHEMA_VERSION = "nodedc.l2-execution-plan/v1"; export const L2_GRAPH_BLUEPRINT_SCHEMA_VERSION = "nodedc.l2-graph-blueprint/v1"; export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/v1"; export const L2_MAPPING_RUNTIME_SCHEMA_VERSION = "nodedc.semantic-mapping-runtime/v1"; -export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.1.0"; +export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.2.0"; +export const L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS = Object.freeze([ + "1.1.0", + L2_EXECUTION_PLAN_COMPILER_VERSION, +]); const HASH = /^(?:sha256:)?[a-f0-9]{64}$/; const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/; @@ -136,7 +140,9 @@ export function validateL2ExecutionPlan(value) { const errors = []; if (!isPlainObject(value)) return result(["executionPlan_must_be_object"]); if (value.schemaVersion !== L2_EXECUTION_PLAN_SCHEMA_VERSION) errors.push("executionPlan.schemaVersion_mismatch"); - if (value.compilerVersion !== L2_EXECUTION_PLAN_COMPILER_VERSION) errors.push("executionPlan.compilerVersion_mismatch"); + if (!L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS.includes(value.compilerVersion)) { + errors.push("executionPlan.compilerVersion_mismatch"); + } if (!Array.isArray(value.steps) || value.steps.length < 5) { errors.push("executionPlan.steps_invalid"); } else { diff --git a/packages/external-provider-contract/test/l2-execution-plan.test.mjs b/packages/external-provider-contract/test/l2-execution-plan.test.mjs index 194d24a..2da9354 100644 --- a/packages/external-provider-contract/test/l2-execution-plan.test.mjs +++ b/packages/external-provider-contract/test/l2-execution-plan.test.mjs @@ -1,6 +1,8 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { + L2_EXECUTION_PLAN_COMPILER_VERSION, + L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, L2_EXECUTION_PLAN_SCHEMA_VERSION, L2_GRAPH_BLUEPRINT_SCHEMA_VERSION, L2_MAPPING_RUNTIME_SCHEMA_VERSION, @@ -12,23 +14,26 @@ import { validateL2ExecutionPlan, } from "../src/index.mjs"; import { - geliosProviderPackageV8, - geliosTelemetryFieldRegistryV1, -} from "../providers/gelios/v8/index.mjs"; + geliosProviderPackageV9, + geliosTelemetryFieldRegistryV2, +} from "../providers/gelios/v9/index.mjs"; -const positionsConnection = instantiateL2Connection(geliosProviderPackageV8, { +const positionsConnection = instantiateL2Connection(geliosProviderPackageV9, { tenantId: "tenant-compiler-fixture", connectionId: "gelios-compiler-fixture", collectionProfileId: "gelios.positions.current.realtime.v7", providerCredentialRef: "ndc-credref:provider-compiler-fixture-0001", }); const positionsPlan = compileL2ExecutionPlan( - geliosProviderPackageV8, + geliosProviderPackageV9, positionsConnection, - { telemetryFieldRegistry: geliosTelemetryFieldRegistryV1 }, + { telemetryFieldRegistry: geliosTelemetryFieldRegistryV2 }, ); assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION); +assert.equal(L2_EXECUTION_PLAN_COMPILER_VERSION, "1.2.0"); +assert.deepEqual(L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, ["1.1.0", "1.2.0"]); +assert.equal(positionsPlan.compilerVersion, "1.2.0"); assert.deepEqual(validateL2ExecutionPlan(positionsPlan), { ok: true, errors: [] }); assert.match(positionsPlan.executionPlanDigest, /^sha256:[a-f0-9]{64}$/); assert.match(positionsPlan.artifacts.packageDigest, /^sha256:[a-f0-9]{64}$/); @@ -106,48 +111,74 @@ assert.deepEqual(unitsRequest.config.request.query, { const semanticMapping = positionsPlan.steps.find((step) => step.kind === "semantic_mapping"); assert.deepEqual( semanticMapping.config.telemetryProjection.entries.map((entry) => entry.reading.id), - ["sensor.param.in0", "sensor.param.in1"], + [ + "sensor.param.in_0", + "sensor.param.in_1", + "sensor.param.in0", + "sensor.param.in1", + ], ); assert.equal(Object.isFrozen(positionsPlan), true); assert.equal(Object.isFrozen(positionsPlan.steps), true); assert.equal(Object.isFrozen(positionsPlan.graphBlueprint), true); const clonedPlan = compileL2ExecutionPlan( - structuredClone(geliosProviderPackageV8), + structuredClone(geliosProviderPackageV9), structuredClone(positionsConnection), - { telemetryFieldRegistry: structuredClone(geliosTelemetryFieldRegistryV1) }, + { telemetryFieldRegistry: structuredClone(geliosTelemetryFieldRegistryV2) }, ); assert.equal(clonedPlan.executionPlanDigest, positionsPlan.executionPlanDigest); +const legacyCompilerPlan = structuredClone(positionsPlan); +legacyCompilerPlan.compilerVersion = "1.1.0"; +legacyCompilerPlan.executionPlanDigest = digestWithout( + legacyCompilerPlan, + "executionPlanDigest", +); +assert.deepEqual(validateL2ExecutionPlan(legacyCompilerPlan), { ok: true, errors: [] }); + +const unsupportedCompilerPlan = structuredClone(positionsPlan); +unsupportedCompilerPlan.compilerVersion = "2.0.0"; +unsupportedCompilerPlan.executionPlanDigest = digestWithout( + unsupportedCompilerPlan, + "executionPlanDigest", +); +assert.equal( + validateL2ExecutionPlan(unsupportedCompilerPlan).errors.includes( + "executionPlan.compilerVersion_mismatch", + ), + true, +); + assert.throws( - () => compileL2ExecutionPlan(geliosProviderPackageV8, positionsConnection), + () => compileL2ExecutionPlan(geliosProviderPackageV9, positionsConnection), /telemetry_registry_required/, ); -const profileConnection = instantiateL2Connection(geliosProviderPackageV8, { +const profileConnection = instantiateL2Connection(geliosProviderPackageV9, { tenantId: "tenant-compiler-fixture", connectionId: "gelios-profile-compiler-fixture", collectionProfileId: "gelios.units.profile.cold.v1", providerCredentialRef: "ndc-credref:provider-compiler-fixture-0001", }); -const profilePlan = compileL2ExecutionPlan(geliosProviderPackageV8, profileConnection); +const profilePlan = compileL2ExecutionPlan(geliosProviderPackageV9, profileConnection); assert.deepEqual(validateL2ExecutionPlan(profilePlan), { ok: true, errors: [] }); assert.equal(profilePlan.artifacts.telemetryRegistryDigest, undefined); assert.notEqual(profilePlan.executionPlanDigest, positionsPlan.executionPlanDigest); assert.deepEqual(profilePlan.graphBlueprint.runtime.mappingRuntime.derivationKinds, []); -for (const collectionProfile of geliosProviderPackageV8.collectionProfiles) { - const connection = instantiateL2Connection(geliosProviderPackageV8, { +for (const collectionProfile of geliosProviderPackageV9.collectionProfiles) { + const connection = instantiateL2Connection(geliosProviderPackageV9, { tenantId: "tenant-all-profiles-fixture", connectionId: `connection-${collectionProfile.id.replaceAll(".", "-")}`, collectionProfileId: collectionProfile.id, providerCredentialRef: "ndc-credref:provider-all-profiles-fixture", }); const plan = compileL2ExecutionPlan( - geliosProviderPackageV8, + geliosProviderPackageV9, connection, collectionProfile.dataProductId === "fleet.positions.current.v5" - ? { telemetryFieldRegistry: geliosTelemetryFieldRegistryV1 } + ? { telemetryFieldRegistry: geliosTelemetryFieldRegistryV2 } : {}, ); assert.deepEqual(validateL2ExecutionPlan(plan), { ok: true, errors: [] }); @@ -157,16 +188,16 @@ for (const collectionProfile of geliosProviderPackageV8.collectionProfiles) { assert.equal(plan.graphBlueprint.edges.length, plan.steps.length + expectedEntrypoints - 2); } -const secondConnection = instantiateL2Connection(geliosProviderPackageV8, { +const secondConnection = instantiateL2Connection(geliosProviderPackageV9, { tenantId: "tenant-compiler-fixture", connectionId: "gelios-compiler-fixture-two", collectionProfileId: "gelios.positions.current.realtime.v7", providerCredentialRef: "ndc-credref:provider-compiler-fixture-0002", }); const secondPlan = compileL2ExecutionPlan( - geliosProviderPackageV8, + geliosProviderPackageV9, secondConnection, - { telemetryFieldRegistry: geliosTelemetryFieldRegistryV1 }, + { telemetryFieldRegistry: geliosTelemetryFieldRegistryV2 }, ); assert.notEqual(secondPlan.executionPlanDigest, positionsPlan.executionPlanDigest); diff --git a/packages/external-provider-contract/test/provider-package.test.mjs b/packages/external-provider-contract/test/provider-package.test.mjs index 44ce484..f1284a3 100644 --- a/packages/external-provider-contract/test/provider-package.test.mjs +++ b/packages/external-provider-contract/test/provider-package.test.mjs @@ -21,6 +21,7 @@ import { geliosProviderPackageV5 } from "../providers/gelios/v5/index.mjs"; import { geliosProviderPackageV6 } from "../providers/gelios/v6/index.mjs"; import { geliosProviderPackageV7 } from "../providers/gelios/v7/index.mjs"; import { geliosProviderPackageV8 } from "../providers/gelios/v8/index.mjs"; +import { geliosProviderPackageV9 } from "../providers/gelios/v9/index.mjs"; import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs"; const expectedFields = [ @@ -47,6 +48,10 @@ assert.deepEqual(validateProviderPackage(geliosProviderPackageV5), { ok: true, e assert.deepEqual(validateProviderPackage(geliosProviderPackageV6), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV7), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV8), { ok: true, errors: [] }); +assert.deepEqual(validateProviderPackage(geliosProviderPackageV9), { ok: true, errors: [] }); +assert.equal(geliosProviderPackageV9.id, "gelios.provider.v9"); +assert.equal(geliosProviderPackageV9.version, "9.0.0"); +assert.equal(geliosProviderPackageV8.id, "gelios.provider.v8"); const profileProduct = geliosProviderPackageV8.dataProducts.find((product) => product.id === "fleet.units.profile.current.v1"); const registeredProfileProduct = JSON.parse(await readFile(new URL( diff --git a/packages/external-provider-contract/test/telemetry-field-registry.test.mjs b/packages/external-provider-contract/test/telemetry-field-registry.test.mjs index 593762d..32c8c38 100644 --- a/packages/external-provider-contract/test/telemetry-field-registry.test.mjs +++ b/packages/external-provider-contract/test/telemetry-field-registry.test.mjs @@ -8,6 +8,9 @@ import { import { geliosTelemetryFieldRegistryV1, } from "../providers/gelios/v8/index.mjs"; +import { + geliosTelemetryFieldRegistryV2, +} from "../providers/gelios/v9/index.mjs"; assert.deepEqual(validateTelemetryFieldRegistry(geliosTelemetryFieldRegistryV1), { ok: true, @@ -31,6 +34,37 @@ assert.equal(projection.entries.some((entry) => entry.sourceKey === "in_0"), fal assert.equal(Object.isFrozen(projection), true); assert.equal(Object.isFrozen(projection.entries), true); +assert.deepEqual(validateTelemetryFieldRegistry(geliosTelemetryFieldRegistryV2), { + ok: true, + errors: [], +}); +const expandedProjection = compileTelemetryFieldProjection( + geliosTelemetryFieldRegistryV2, + { + providerPackageId: "gelios.provider.v9", + providerPackageVersion: "9.0.0", + dataProductId: "fleet.positions.current.v5", + surface: "data_product", + }, +); +assert.deepEqual( + expandedProjection.entries.map((entry) => entry.sourceKey), + ["in_0", "in_1", "in0", "in1"], +); +assert.deepEqual( + expandedProjection.entries.map((entry) => entry.reading.id), + [ + "sensor.param.in_0", + "sensor.param.in_1", + "sensor.param.in0", + "sensor.param.in1", + ], +); +assert.notEqual( + digestTelemetryFieldRegistry(geliosTelemetryFieldRegistryV2), + digestTelemetryFieldRegistry(geliosTelemetryFieldRegistryV1), +); + const observedExposed = structuredClone(geliosTelemetryFieldRegistryV1); observedExposed.entries.find((entry) => entry.sourceKey === "in_0").allowedSurfaces.push("foundry"); assert.equal(