feat(provider-contract): compile governed L2 execution plans
This commit is contained in:
parent
6f9da1b677
commit
a5a5644dbf
|
|
@ -48,6 +48,25 @@ read-capability передаёт все entities, которые provider воз
|
||||||
boundary запрещены; сортировка и видимость принадлежат Data Product consumer и
|
boundary запрещены; сортировка и видимость принадлежат Data Product consumer и
|
||||||
Foundry.
|
Foundry.
|
||||||
|
|
||||||
|
`compileL2ExecutionPlan` превращает package + connection instance в
|
||||||
|
детерминированный `nodedc.l2-execution-plan/v1`: разрешает generic шаги
|
||||||
|
collection/request/extract/mapping/publish, прикладывает полные декларативные
|
||||||
|
request/mapping contracts и фиксирует SHA-256 digests package, profile,
|
||||||
|
template, mapping, field policy, Data Product и telemetry registry. В
|
||||||
|
компиляторе нет веток по provider ID. Provider-specific URL, source paths и
|
||||||
|
параметры являются данными immutable package. После материализации Engine
|
||||||
|
`attestL2ExecutionPlanMaterialization` связывает exact plan digest с exact
|
||||||
|
graph revision/digest и точным списком materialized steps.
|
||||||
|
|
||||||
|
Динамическая телеметрия проходит отдельный
|
||||||
|
`nodedc.telemetry-field-registry/v1`. Реестр хранит только имена source
|
||||||
|
parameters, типы, статус классификации, sensitivity и разрешённые surfaces —
|
||||||
|
никогда значения. Только `approved + operational` entries могут войти в
|
||||||
|
Data Product/analytics/Foundry projection. `observed`, PII/identifier,
|
||||||
|
command и restricted entries остаются audit-only; wildcard source key
|
||||||
|
запрещён. Поэтому появление нового параметра у provider не меняет ядро и не
|
||||||
|
открывает его автоматически потребителям.
|
||||||
|
|
||||||
Текущий production-shaped package — `providers/gelios/v5`. Он сохраняет два
|
Текущий production-shaped package — `providers/gelios/v5`. Он сохраняет два
|
||||||
официальных Gelios REST safe-read: `GET /api/v1/users/me/monitoring-config` и
|
официальных Gelios REST safe-read: `GET /api/v1/users/me/monitoring-config` и
|
||||||
`GET /api/v1/units?incltrip=true`, а также точный immutable output
|
`GET /api/v1/units?incltrip=true`, а также точный immutable output
|
||||||
|
|
|
||||||
|
|
@ -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/provider-capability-catalog.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/telemetry-field-registry.test.mjs && node test/l2-execution-plan.test.mjs && node test/zone-source.test.mjs && node test/engine-private-extension.test.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ export {
|
||||||
GELIOS_UNIT_PROFILE_ONTOLOGY_REVISION,
|
GELIOS_UNIT_PROFILE_ONTOLOGY_REVISION,
|
||||||
geliosProviderPackageV8,
|
geliosProviderPackageV8,
|
||||||
} from "./package.mjs";
|
} from "./package.mjs";
|
||||||
|
export { geliosTelemetryFieldRegistryV1 } from "./telemetry-field-registry.mjs";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import {
|
||||||
|
TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION,
|
||||||
|
} from "../../../src/telemetry-field-registry.mjs";
|
||||||
|
import {
|
||||||
|
GELIOS_PROVIDER_PACKAGE_ID,
|
||||||
|
GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
} from "./package.mjs";
|
||||||
|
import {
|
||||||
|
GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
} from "../v7/package.mjs";
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
schemaVersion: TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION,
|
||||||
|
id: "gelios.units.current.telemetry.v1",
|
||||||
|
version: "1.0.0",
|
||||||
|
providerPackage: {
|
||||||
|
id: GELIOS_PROVIDER_PACKAGE_ID,
|
||||||
|
version: GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
},
|
||||||
|
dataProductId: GELIOS_POSITIONS_DATA_PRODUCT_ID,
|
||||||
|
sourceNamespace: "gelios.units.configured_sensor_parameter",
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
id: "gelios.sensor-parameter.in0",
|
||||||
|
sourceKey: "in0",
|
||||||
|
status: "approved",
|
||||||
|
sensitivity: "operational",
|
||||||
|
valueType: "number",
|
||||||
|
semanticReading: {
|
||||||
|
id: "sensor.param.in0",
|
||||||
|
labelSource: "provider_configured",
|
||||||
|
unitSource: "provider_configured",
|
||||||
|
},
|
||||||
|
allowedSurfaces: ["audit", "data_product", "analytics", "foundry"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gelios.sensor-parameter.in1",
|
||||||
|
sourceKey: "in1",
|
||||||
|
status: "approved",
|
||||||
|
sensitivity: "operational",
|
||||||
|
valueType: "number",
|
||||||
|
semanticReading: {
|
||||||
|
id: "sensor.param.in1",
|
||||||
|
labelSource: "provider_configured",
|
||||||
|
unitSource: "provider_configured",
|
||||||
|
},
|
||||||
|
allowedSurfaces: ["audit", "data_product", "analytics", "foundry"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gelios.sensor-parameter.in-underscore-0",
|
||||||
|
sourceKey: "in_0",
|
||||||
|
status: "observed",
|
||||||
|
sensitivity: "unclassified",
|
||||||
|
valueType: "unknown",
|
||||||
|
allowedSurfaces: ["audit"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gelios.sensor-parameter.in-underscore-1",
|
||||||
|
sourceKey: "in_1",
|
||||||
|
status: "observed",
|
||||||
|
sensitivity: "unclassified",
|
||||||
|
valueType: "unknown",
|
||||||
|
allowedSurfaces: ["audit"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const geliosTelemetryFieldRegistryV1 = 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;
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,21 @@ export {
|
||||||
TELEMETRY_READINGS_MAX_ITEMS,
|
TELEMETRY_READINGS_MAX_ITEMS,
|
||||||
isBoundedTelemetryReadings,
|
isBoundedTelemetryReadings,
|
||||||
} from "./telemetry-readings.mjs";
|
} from "./telemetry-readings.mjs";
|
||||||
|
export {
|
||||||
|
TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION,
|
||||||
|
TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION,
|
||||||
|
compileTelemetryFieldProjection,
|
||||||
|
digestTelemetryFieldRegistry,
|
||||||
|
validateTelemetryFieldRegistry,
|
||||||
|
} from "./telemetry-field-registry.mjs";
|
||||||
|
export {
|
||||||
|
L2_EXECUTION_PLAN_COMPILER_VERSION,
|
||||||
|
L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||||
|
L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
|
||||||
|
attestL2ExecutionPlanMaterialization,
|
||||||
|
compileL2ExecutionPlan,
|
||||||
|
validateL2ExecutionPlan,
|
||||||
|
} from "./l2-execution-plan.mjs";
|
||||||
export {
|
export {
|
||||||
ZONE_SOURCE_ADAPTERS,
|
ZONE_SOURCE_ADAPTERS,
|
||||||
ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
|
ZONE_SOURCE_GENERATION_SCHEMA_VERSION,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,333 @@
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { validateProviderPackage } from "./provider-package.mjs";
|
||||||
|
import {
|
||||||
|
compileTelemetryFieldProjection,
|
||||||
|
validateTelemetryFieldRegistry,
|
||||||
|
} from "./telemetry-field-registry.mjs";
|
||||||
|
|
||||||
|
export const L2_EXECUTION_PLAN_SCHEMA_VERSION = "nodedc.l2-execution-plan/v1";
|
||||||
|
export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/v1";
|
||||||
|
export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.0.0";
|
||||||
|
|
||||||
|
const HASH = /^(?:sha256:)?[a-f0-9]{64}$/;
|
||||||
|
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||||
|
const CREDENTIAL_REF = /^ndc-credref:[A-Za-z0-9._:-]{8,255}$/;
|
||||||
|
const COMPILE_OPTION_KEYS = new Set(["telemetryFieldRegistry"]);
|
||||||
|
const RECEIPT_OPTION_KEYS = new Set(["graphRevision", "graphDigest", "materializedStepIds"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve one immutable provider package plus one connection instance into a
|
||||||
|
* deterministic, provider-neutral execution plan. Provider-specific knowledge
|
||||||
|
* stays in package/registry data; compiler branching is only by generic step
|
||||||
|
* kind.
|
||||||
|
*/
|
||||||
|
export function compileL2ExecutionPlan(providerPackage, connectionInstance, options = {}) {
|
||||||
|
const packageValidation = validateProviderPackage(providerPackage);
|
||||||
|
if (!packageValidation.ok) {
|
||||||
|
throw new Error(`provider_package_invalid:${packageValidation.errors.join(",")}`);
|
||||||
|
}
|
||||||
|
validateCompileOptions(options);
|
||||||
|
validateConnectionInstance(providerPackage, connectionInstance);
|
||||||
|
|
||||||
|
const profile = requireById(
|
||||||
|
providerPackage.collectionProfiles,
|
||||||
|
connectionInstance.collectionProfileId,
|
||||||
|
"collection_profile",
|
||||||
|
);
|
||||||
|
const template = requireById(providerPackage.l2Templates, profile.l2TemplateId, "l2_template");
|
||||||
|
if (template.id !== connectionInstance.l2TemplateId) {
|
||||||
|
throw new Error("l2_execution_plan_connection_template_mismatch");
|
||||||
|
}
|
||||||
|
const mapping = requireById(providerPackage.mappingContracts, profile.mappingContractId, "mapping_contract");
|
||||||
|
const fieldPolicy = requireById(providerPackage.fieldPolicies, profile.fieldPolicyId, "field_policy");
|
||||||
|
const dataProduct = requireById(providerPackage.dataProducts, profile.dataProductId, "data_product");
|
||||||
|
const requiresTelemetryRegistry = Object.values(dataProduct.fieldContracts || {})
|
||||||
|
.some((contract) => contract?.type === "telemetry_readings");
|
||||||
|
|
||||||
|
let telemetryProjection;
|
||||||
|
if (options.telemetryFieldRegistry !== undefined) {
|
||||||
|
const registryValidation = validateTelemetryFieldRegistry(options.telemetryFieldRegistry);
|
||||||
|
if (!registryValidation.ok) {
|
||||||
|
throw new Error(`telemetry_field_registry_invalid:${registryValidation.errors.join(",")}`);
|
||||||
|
}
|
||||||
|
telemetryProjection = compileTelemetryFieldProjection(options.telemetryFieldRegistry, {
|
||||||
|
providerPackageId: providerPackage.id,
|
||||||
|
providerPackageVersion: providerPackage.version,
|
||||||
|
dataProductId: dataProduct.id,
|
||||||
|
surface: "data_product",
|
||||||
|
});
|
||||||
|
} else if (requiresTelemetryRegistry) {
|
||||||
|
throw new Error("l2_execution_plan_telemetry_registry_required");
|
||||||
|
}
|
||||||
|
if (requiresTelemetryRegistry && telemetryProjection.entries.length === 0) {
|
||||||
|
throw new Error("l2_execution_plan_telemetry_projection_empty");
|
||||||
|
}
|
||||||
|
if (!requiresTelemetryRegistry && telemetryProjection !== undefined) {
|
||||||
|
throw new Error("l2_execution_plan_telemetry_registry_not_applicable");
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifacts = {
|
||||||
|
packageDigest: canonicalDigest(providerPackage),
|
||||||
|
collectionProfileDigest: canonicalDigest(profile),
|
||||||
|
l2TemplateDigest: canonicalDigest(template),
|
||||||
|
mappingContractDigest: canonicalDigest(mapping),
|
||||||
|
fieldPolicyDigest: canonicalDigest(fieldPolicy),
|
||||||
|
dataProductDigest: canonicalDigest(dataProduct),
|
||||||
|
...(telemetryProjection ? { telemetryRegistryDigest: telemetryProjection.registry.digest } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = template.steps.map((step) => compileStep({
|
||||||
|
step,
|
||||||
|
providerPackage,
|
||||||
|
profile,
|
||||||
|
mapping,
|
||||||
|
fieldPolicy,
|
||||||
|
dataProduct,
|
||||||
|
telemetryProjection,
|
||||||
|
artifacts,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const planWithoutDigest = {
|
||||||
|
schemaVersion: L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||||
|
compilerVersion: L2_EXECUTION_PLAN_COMPILER_VERSION,
|
||||||
|
package: {
|
||||||
|
id: providerPackage.id,
|
||||||
|
providerId: providerPackage.providerId,
|
||||||
|
version: providerPackage.version,
|
||||||
|
},
|
||||||
|
connection: {
|
||||||
|
tenantId: connectionInstance.tenantId,
|
||||||
|
connectionId: connectionInstance.connectionId,
|
||||||
|
collectionProfileId: profile.id,
|
||||||
|
l2TemplateId: template.id,
|
||||||
|
},
|
||||||
|
bindings: {
|
||||||
|
provider: structuredClone(connectionInstance.credentialRefs.provider),
|
||||||
|
publisher: structuredClone(connectionInstance.systemBindings.publisher),
|
||||||
|
},
|
||||||
|
collectionProfile: structuredClone(profile),
|
||||||
|
artifacts,
|
||||||
|
steps,
|
||||||
|
};
|
||||||
|
|
||||||
|
return deepFreeze({
|
||||||
|
...planWithoutDigest,
|
||||||
|
executionPlanDigest: canonicalDigest(planWithoutDigest),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!Array.isArray(value.steps) || value.steps.length < 5) {
|
||||||
|
errors.push("executionPlan.steps_invalid");
|
||||||
|
} else {
|
||||||
|
const ids = value.steps.map((step) => step?.id);
|
||||||
|
if (ids.some((id) => typeof id !== "string" || !IDENTIFIER.test(id))) errors.push("executionPlan.steps_id_invalid");
|
||||||
|
if (new Set(ids).size !== ids.length) errors.push("executionPlan.steps_id_must_be_unique");
|
||||||
|
}
|
||||||
|
if (!HASH.test(String(value.executionPlanDigest || ""))) {
|
||||||
|
errors.push("executionPlan.executionPlanDigest_invalid");
|
||||||
|
} else {
|
||||||
|
const descriptor = structuredClone(value);
|
||||||
|
delete descriptor.executionPlanDigest;
|
||||||
|
if (normalizeDigest(value.executionPlanDigest) !== canonicalDigest(descriptor)) {
|
||||||
|
errors.push("executionPlan.executionPlanDigest_mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a successful Engine graph materialization back to the exact compiled
|
||||||
|
* plan without teaching this package how Engine stores or renders its graph.
|
||||||
|
*/
|
||||||
|
export function attestL2ExecutionPlanMaterialization(executionPlan, options) {
|
||||||
|
const validation = validateL2ExecutionPlan(executionPlan);
|
||||||
|
if (!validation.ok) {
|
||||||
|
throw new Error(`l2_execution_plan_invalid:${validation.errors.join(",")}`);
|
||||||
|
}
|
||||||
|
if (!isPlainObject(options)) throw new Error("l2_materialization_options_must_be_object");
|
||||||
|
const errors = [];
|
||||||
|
rejectUnknownKeys(options, RECEIPT_OPTION_KEYS, "options", errors);
|
||||||
|
if (typeof options.graphRevision !== "string" || !options.graphRevision.trim() || options.graphRevision.length > 160) {
|
||||||
|
errors.push("options.graphRevision_invalid");
|
||||||
|
}
|
||||||
|
if (!HASH.test(String(options.graphDigest || ""))) errors.push("options.graphDigest_invalid");
|
||||||
|
const expectedStepIds = executionPlan.steps.map((step) => step.id);
|
||||||
|
if (!Array.isArray(options.materializedStepIds)
|
||||||
|
|| options.materializedStepIds.length !== expectedStepIds.length
|
||||||
|
|| options.materializedStepIds.some((id, index) => id !== expectedStepIds[index])) {
|
||||||
|
errors.push("options.materializedStepIds_must_exactly_match_execution_plan");
|
||||||
|
}
|
||||||
|
if (errors.length) {
|
||||||
|
throw new Error(`l2_materialization_invalid:${[...new Set(errors)].join(",")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiptWithoutDigest = {
|
||||||
|
schemaVersion: L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
|
||||||
|
compilerVersion: executionPlan.compilerVersion,
|
||||||
|
executionPlanDigest: executionPlan.executionPlanDigest,
|
||||||
|
graphRevision: options.graphRevision,
|
||||||
|
graphDigest: normalizeDigest(options.graphDigest),
|
||||||
|
materializedStepIds: [...options.materializedStepIds],
|
||||||
|
};
|
||||||
|
return deepFreeze({
|
||||||
|
...receiptWithoutDigest,
|
||||||
|
receiptDigest: canonicalDigest(receiptWithoutDigest),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileStep(context) {
|
||||||
|
const { step, providerPackage, profile, mapping, fieldPolicy, dataProduct, telemetryProjection, artifacts } = context;
|
||||||
|
const base = { id: step.id, kind: step.kind };
|
||||||
|
if (step.kind === "collection_trigger") {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
config: {
|
||||||
|
mode: profile.mode,
|
||||||
|
...(profile.schedule ? { schedule: structuredClone(profile.schedule) } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step.kind === "provider_request") {
|
||||||
|
const capability = requireById(providerPackage.capabilities, step.capabilityId, "capability");
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
config: {
|
||||||
|
capabilityId: capability.id,
|
||||||
|
capabilityDigest: canonicalDigest(capability),
|
||||||
|
authModeId: capability.authModeId,
|
||||||
|
credentialBinding: "provider",
|
||||||
|
request: structuredClone(capability.request),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step.kind === "extract_items") {
|
||||||
|
const capability = requireById(providerPackage.capabilities, step.capabilityId, "capability");
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
config: {
|
||||||
|
capabilityId: capability.id,
|
||||||
|
capabilityDigest: canonicalDigest(capability),
|
||||||
|
response: structuredClone(capability.request.response),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step.kind === "semantic_mapping") {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
config: {
|
||||||
|
mappingContractId: mapping.id,
|
||||||
|
mappingContractDigest: artifacts.mappingContractDigest,
|
||||||
|
fieldPolicyId: fieldPolicy.id,
|
||||||
|
fieldPolicyDigest: artifacts.fieldPolicyDigest,
|
||||||
|
dataProductId: dataProduct.id,
|
||||||
|
dataProductDigest: artifacts.dataProductDigest,
|
||||||
|
mappingContract: structuredClone(mapping),
|
||||||
|
...(telemetryProjection ? { telemetryProjection: structuredClone(telemetryProjection) } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step.kind === "data_product_publish") {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
config: {
|
||||||
|
dataProductId: dataProduct.id,
|
||||||
|
dataProductDigest: artifacts.dataProductDigest,
|
||||||
|
nodeType: step.nodeType,
|
||||||
|
credentialBinding: "publisher",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`l2_execution_plan_step_kind_unsupported:${String(step.kind)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConnectionInstance(providerPackage, value) {
|
||||||
|
if (!isPlainObject(value)) throw new Error("l2_execution_plan_connection_must_be_object");
|
||||||
|
if (value.schemaVersion !== "nodedc.l2-connection-instance/v1") {
|
||||||
|
throw new Error("l2_execution_plan_connection_schema_mismatch");
|
||||||
|
}
|
||||||
|
if (value.package?.id !== providerPackage.id
|
||||||
|
|| value.package?.providerId !== providerPackage.providerId
|
||||||
|
|| value.package?.version !== providerPackage.version) {
|
||||||
|
throw new Error("l2_execution_plan_connection_package_mismatch");
|
||||||
|
}
|
||||||
|
if (typeof value.tenantId !== "string" || !IDENTIFIER.test(value.tenantId)) {
|
||||||
|
throw new Error("l2_execution_plan_tenant_id_invalid");
|
||||||
|
}
|
||||||
|
if (typeof value.connectionId !== "string" || !IDENTIFIER.test(value.connectionId)) {
|
||||||
|
throw new Error("l2_execution_plan_connection_id_invalid");
|
||||||
|
}
|
||||||
|
if (!CREDENTIAL_REF.test(String(value.credentialRefs?.provider?.reference || ""))
|
||||||
|
|| value.credentialRefs?.provider?.owner !== "ndc_l2_credentials") {
|
||||||
|
throw new Error("l2_execution_plan_provider_credential_ref_invalid");
|
||||||
|
}
|
||||||
|
const publisher = value.systemBindings?.publisher;
|
||||||
|
if (publisher?.owner !== "ndc_l2_credentials"
|
||||||
|
|| publisher?.management !== "control_plane_managed"
|
||||||
|
|| publisher?.desiredState !== "bound"
|
||||||
|
|| publisher?.status !== "unresolved") {
|
||||||
|
throw new Error("l2_execution_plan_publisher_binding_invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCompileOptions(options) {
|
||||||
|
if (!isPlainObject(options)) throw new Error("l2_execution_plan_options_must_be_object");
|
||||||
|
const errors = [];
|
||||||
|
rejectUnknownKeys(options, COMPILE_OPTION_KEYS, "options", errors);
|
||||||
|
if (errors.length) throw new Error(`l2_execution_plan_options_invalid:${errors.join(",")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireById(items, id, type) {
|
||||||
|
const value = Array.isArray(items) ? items.find((item) => item?.id === id) : undefined;
|
||||||
|
if (!value) throw new Error(`l2_execution_plan_${type}_missing:${String(id)}`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalDigest(value) {
|
||||||
|
return `sha256:${createHash("sha256").update(stableJson(value), "utf8").digest("hex")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDigest(value) {
|
||||||
|
const text = String(value);
|
||||||
|
return text.startsWith("sha256:") ? text : `sha256:${text}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableJson(value) {
|
||||||
|
return JSON.stringify(stableValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableValue(value) {
|
||||||
|
if (Array.isArray(value)) return value.map(stableValue);
|
||||||
|
if (!isPlainObject(value)) return value;
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(value)
|
||||||
|
.filter((key) => value[key] !== undefined)
|
||||||
|
.sort()
|
||||||
|
.map((key) => [key, stableValue(value[key])]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectUnknownKeys(value, allowed, path, errors) {
|
||||||
|
for (const key of Object.keys(value)) {
|
||||||
|
if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function result(errors) {
|
||||||
|
return { ok: errors.length === 0, errors: [...new Set(errors)] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
|
export const TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION = "nodedc.telemetry-field-registry/v1";
|
||||||
|
export const TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION = "nodedc.telemetry-field-projection/v1";
|
||||||
|
|
||||||
|
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||||
|
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
||||||
|
const SOURCE_KEY = /^[A-Za-z_][A-Za-z0-9_.:-]{0,127}$/;
|
||||||
|
const VALUE_TYPES = new Set(["unknown", "number", "boolean", "string"]);
|
||||||
|
const STATUSES = new Set(["observed", "classified", "approved", "restricted", "deprecated"]);
|
||||||
|
const SENSITIVITIES = new Set(["operational", "unclassified", "identifier", "personal", "command", "restricted"]);
|
||||||
|
const SURFACES = new Set(["audit", "data_product", "analytics", "foundry"]);
|
||||||
|
const REGISTRY_KEYS = new Set([
|
||||||
|
"schemaVersion",
|
||||||
|
"id",
|
||||||
|
"version",
|
||||||
|
"providerPackage",
|
||||||
|
"dataProductId",
|
||||||
|
"sourceNamespace",
|
||||||
|
"entries",
|
||||||
|
]);
|
||||||
|
const PACKAGE_KEYS = new Set(["id", "version"]);
|
||||||
|
const ENTRY_KEYS = new Set([
|
||||||
|
"id",
|
||||||
|
"sourceKey",
|
||||||
|
"status",
|
||||||
|
"sensitivity",
|
||||||
|
"valueType",
|
||||||
|
"semanticReading",
|
||||||
|
"allowedSurfaces",
|
||||||
|
]);
|
||||||
|
const READING_KEYS = new Set(["id", "labelSource", "label", "unitSource", "unit"]);
|
||||||
|
const LABEL_SOURCES = new Set(["registry", "provider_configured"]);
|
||||||
|
const UNIT_SOURCES = new Set(["none", "registry", "provider_configured"]);
|
||||||
|
const PROJECTION_OPTION_KEYS = new Set([
|
||||||
|
"providerPackageId",
|
||||||
|
"providerPackageVersion",
|
||||||
|
"dataProductId",
|
||||||
|
"surface",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a values-free, provider-scoped registry that classifies dynamic
|
||||||
|
* telemetry parameters before any Data Product or interface can expose them.
|
||||||
|
*/
|
||||||
|
export function validateTelemetryFieldRegistry(value) {
|
||||||
|
const errors = [];
|
||||||
|
if (!isPlainObject(value)) return result(["registry_must_be_object"]);
|
||||||
|
rejectUnknownKeys(value, REGISTRY_KEYS, "registry", errors);
|
||||||
|
if (value.schemaVersion !== TELEMETRY_FIELD_REGISTRY_SCHEMA_VERSION) {
|
||||||
|
errors.push("registry.schemaVersion_mismatch");
|
||||||
|
}
|
||||||
|
requiredIdentifier(value.id, "registry.id", errors);
|
||||||
|
requiredSemver(value.version, "registry.version", errors);
|
||||||
|
requiredIdentifier(value.dataProductId, "registry.dataProductId", errors);
|
||||||
|
requiredIdentifier(value.sourceNamespace, "registry.sourceNamespace", errors);
|
||||||
|
|
||||||
|
if (!isPlainObject(value.providerPackage)) {
|
||||||
|
errors.push("registry.providerPackage_must_be_object");
|
||||||
|
} else {
|
||||||
|
rejectUnknownKeys(value.providerPackage, PACKAGE_KEYS, "registry.providerPackage", errors);
|
||||||
|
requiredIdentifier(value.providerPackage.id, "registry.providerPackage.id", errors);
|
||||||
|
requiredSemver(value.providerPackage.version, "registry.providerPackage.version", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(value.entries) || value.entries.length === 0 || value.entries.length > 512) {
|
||||||
|
errors.push("registry.entries_must_be_nonempty_bounded_array");
|
||||||
|
} else {
|
||||||
|
const entryIds = new Set();
|
||||||
|
const sourceKeys = new Set();
|
||||||
|
const approvedReadingIds = new Set();
|
||||||
|
value.entries.forEach((entry, index) => {
|
||||||
|
const path = `registry.entries[${index}]`;
|
||||||
|
if (!isPlainObject(entry)) {
|
||||||
|
errors.push(`${path}_must_be_object`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rejectUnknownKeys(entry, ENTRY_KEYS, path, errors);
|
||||||
|
requiredIdentifier(entry.id, `${path}.id`, errors);
|
||||||
|
if (entryIds.has(entry.id)) errors.push(`${path}.id_must_be_unique`);
|
||||||
|
entryIds.add(entry.id);
|
||||||
|
if (typeof entry.sourceKey !== "string" || !SOURCE_KEY.test(entry.sourceKey)) {
|
||||||
|
errors.push(`${path}.sourceKey_invalid`);
|
||||||
|
}
|
||||||
|
if (sourceKeys.has(entry.sourceKey)) errors.push(`${path}.sourceKey_must_be_unique`);
|
||||||
|
sourceKeys.add(entry.sourceKey);
|
||||||
|
if (!STATUSES.has(entry.status)) errors.push(`${path}.status_invalid`);
|
||||||
|
if (!SENSITIVITIES.has(entry.sensitivity)) errors.push(`${path}.sensitivity_invalid`);
|
||||||
|
if (!VALUE_TYPES.has(entry.valueType)) errors.push(`${path}.valueType_invalid`);
|
||||||
|
validateSurfaces(entry.allowedSurfaces, path, errors);
|
||||||
|
validateSemanticReading(entry.semanticReading, path, errors);
|
||||||
|
|
||||||
|
const surfaces = new Set(Array.isArray(entry.allowedSurfaces) ? entry.allowedSurfaces : []);
|
||||||
|
const externallyExposed = [...surfaces].some((surface) => surface !== "audit");
|
||||||
|
if (entry.status === "approved") {
|
||||||
|
if (!isPlainObject(entry.semanticReading)) errors.push(`${path}.approved_requires_semantic_reading`);
|
||||||
|
if (entry.sensitivity !== "operational") errors.push(`${path}.approved_must_be_operational`);
|
||||||
|
if (entry.valueType === "unknown") errors.push(`${path}.approved_valueType_must_be_classified`);
|
||||||
|
if (!surfaces.has("data_product")) errors.push(`${path}.approved_requires_data_product_surface`);
|
||||||
|
const readingId = entry.semanticReading?.id;
|
||||||
|
if (typeof readingId === "string") {
|
||||||
|
if (approvedReadingIds.has(readingId)) errors.push(`${path}.semanticReading.id_must_be_unique`);
|
||||||
|
approvedReadingIds.add(readingId);
|
||||||
|
}
|
||||||
|
} else if (externallyExposed) {
|
||||||
|
errors.push(`${path}.nonapproved_surface_must_be_audit_only`);
|
||||||
|
}
|
||||||
|
if (new Set(["observed", "restricted"]).has(entry.status) && entry.semanticReading !== undefined) {
|
||||||
|
errors.push(`${path}.${entry.status}_must_not_claim_semantic_reading`);
|
||||||
|
}
|
||||||
|
if (entry.status === "observed" && entry.sensitivity !== "unclassified") {
|
||||||
|
errors.push(`${path}.observed_must_be_unclassified`);
|
||||||
|
}
|
||||||
|
if (entry.status === "restricted" && !new Set(["identifier", "personal", "command", "restricted"]).has(entry.sensitivity)) {
|
||||||
|
errors.push(`${path}.restricted_sensitivity_invalid`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile an exact, values-free allowlist for one consumer surface. Unknown,
|
||||||
|
* observed, restricted and deprecated provider parameters are omitted.
|
||||||
|
*/
|
||||||
|
export function compileTelemetryFieldProjection(registry, options) {
|
||||||
|
const validation = validateTelemetryFieldRegistry(registry);
|
||||||
|
if (!validation.ok) {
|
||||||
|
throw new Error(`telemetry_field_registry_invalid:${validation.errors.join(",")}`);
|
||||||
|
}
|
||||||
|
if (!isPlainObject(options)) throw new Error("telemetry_projection_options_must_be_object");
|
||||||
|
const optionErrors = [];
|
||||||
|
rejectUnknownKeys(options, PROJECTION_OPTION_KEYS, "options", optionErrors);
|
||||||
|
requiredIdentifier(options.providerPackageId, "options.providerPackageId", optionErrors);
|
||||||
|
requiredSemver(options.providerPackageVersion, "options.providerPackageVersion", optionErrors);
|
||||||
|
requiredIdentifier(options.dataProductId, "options.dataProductId", optionErrors);
|
||||||
|
if (!SURFACES.has(options.surface)) optionErrors.push("options.surface_invalid");
|
||||||
|
if (options.providerPackageId !== registry.providerPackage.id) optionErrors.push("options.providerPackageId_mismatch");
|
||||||
|
if (options.providerPackageVersion !== registry.providerPackage.version) optionErrors.push("options.providerPackageVersion_mismatch");
|
||||||
|
if (options.dataProductId !== registry.dataProductId) optionErrors.push("options.dataProductId_mismatch");
|
||||||
|
if (optionErrors.length) {
|
||||||
|
throw new Error(`telemetry_projection_invalid:${[...new Set(optionErrors)].join(",")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = registry.entries
|
||||||
|
.filter((entry) => entry.status === "approved" && entry.allowedSurfaces.includes(options.surface))
|
||||||
|
.map((entry) => ({
|
||||||
|
registryEntryId: entry.id,
|
||||||
|
sourceNamespace: registry.sourceNamespace,
|
||||||
|
sourceKey: entry.sourceKey,
|
||||||
|
valueType: entry.valueType,
|
||||||
|
reading: structuredClone(entry.semanticReading),
|
||||||
|
}))
|
||||||
|
.sort((left, right) => left.sourceKey.localeCompare(right.sourceKey));
|
||||||
|
|
||||||
|
return deepFreeze({
|
||||||
|
schemaVersion: TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION,
|
||||||
|
registry: {
|
||||||
|
id: registry.id,
|
||||||
|
version: registry.version,
|
||||||
|
digest: digestTelemetryFieldRegistry(registry),
|
||||||
|
},
|
||||||
|
providerPackage: structuredClone(registry.providerPackage),
|
||||||
|
dataProductId: registry.dataProductId,
|
||||||
|
surface: options.surface,
|
||||||
|
entries,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function digestTelemetryFieldRegistry(registry) {
|
||||||
|
const validation = validateTelemetryFieldRegistry(registry);
|
||||||
|
if (!validation.ok) {
|
||||||
|
throw new Error(`telemetry_field_registry_invalid:${validation.errors.join(",")}`);
|
||||||
|
}
|
||||||
|
return canonicalDigest(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSurfaces(value, path, errors) {
|
||||||
|
if (!Array.isArray(value) || value.length === 0 || value.length > SURFACES.size) {
|
||||||
|
errors.push(`${path}.allowedSurfaces_must_be_nonempty_bounded_array`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (new Set(value).size !== value.length) errors.push(`${path}.allowedSurfaces_must_be_unique`);
|
||||||
|
value.forEach((surface) => {
|
||||||
|
if (!SURFACES.has(surface)) errors.push(`${path}.allowedSurfaces_contains_invalid_surface`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSemanticReading(value, path, errors) {
|
||||||
|
if (value === undefined) return;
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
errors.push(`${path}.semanticReading_must_be_object`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rejectUnknownKeys(value, READING_KEYS, `${path}.semanticReading`, errors);
|
||||||
|
requiredIdentifier(value.id, `${path}.semanticReading.id`, errors);
|
||||||
|
if (!LABEL_SOURCES.has(value.labelSource)) {
|
||||||
|
errors.push(`${path}.semanticReading.labelSource_invalid`);
|
||||||
|
}
|
||||||
|
if (value.labelSource === "registry"
|
||||||
|
&& (typeof value.label !== "string" || !value.label.trim() || value.label.length > 160)) {
|
||||||
|
errors.push(`${path}.semanticReading.label_invalid`);
|
||||||
|
}
|
||||||
|
if (value.labelSource === "provider_configured"
|
||||||
|
&& value.label !== undefined
|
||||||
|
&& (typeof value.label !== "string" || !value.label.trim() || value.label.length > 160)) {
|
||||||
|
errors.push(`${path}.semanticReading.label_invalid`);
|
||||||
|
}
|
||||||
|
if (!UNIT_SOURCES.has(value.unitSource)) {
|
||||||
|
errors.push(`${path}.semanticReading.unitSource_invalid`);
|
||||||
|
}
|
||||||
|
if (value.unitSource === "registry"
|
||||||
|
&& (typeof value.unit !== "string" || !value.unit.trim() || value.unit.length > 32)) {
|
||||||
|
errors.push(`${path}.semanticReading.unit_invalid`);
|
||||||
|
}
|
||||||
|
if (value.unitSource === "none" && value.unit !== undefined) {
|
||||||
|
errors.push(`${path}.semanticReading.unit_not_allowed`);
|
||||||
|
}
|
||||||
|
if (value.unit !== undefined && (typeof value.unit !== "string" || value.unit.length > 32)) {
|
||||||
|
errors.push(`${path}.semanticReading.unit_invalid`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalDigest(value) {
|
||||||
|
return `sha256:${createHash("sha256").update(stableJson(value), "utf8").digest("hex")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableJson(value) {
|
||||||
|
return JSON.stringify(stableValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableValue(value) {
|
||||||
|
if (Array.isArray(value)) return value.map(stableValue);
|
||||||
|
if (!isPlainObject(value)) return value;
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(value)
|
||||||
|
.filter((key) => value[key] !== undefined)
|
||||||
|
.sort()
|
||||||
|
.map((key) => [key, stableValue(value[key])]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectUnknownKeys(value, allowed, path, errors) {
|
||||||
|
for (const key of Object.keys(value)) {
|
||||||
|
if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredIdentifier(value, path, errors) {
|
||||||
|
if (typeof value !== "string" || !IDENTIFIER.test(value)) errors.push(`${path}_invalid`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredSemver(value, path, errors) {
|
||||||
|
if (typeof value !== "string" || !SEMVER.test(value)) errors.push(`${path}_invalid`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function result(errors) {
|
||||||
|
return { ok: errors.length === 0, errors: [...new Set(errors)] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import {
|
||||||
|
L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||||
|
L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
|
||||||
|
attestL2ExecutionPlanMaterialization,
|
||||||
|
compileL2ExecutionPlan,
|
||||||
|
instantiateL2Connection,
|
||||||
|
validateL2ExecutionPlan,
|
||||||
|
} from "../src/index.mjs";
|
||||||
|
import {
|
||||||
|
geliosProviderPackageV8,
|
||||||
|
geliosTelemetryFieldRegistryV1,
|
||||||
|
} from "../providers/gelios/v8/index.mjs";
|
||||||
|
|
||||||
|
const positionsConnection = instantiateL2Connection(geliosProviderPackageV8, {
|
||||||
|
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,
|
||||||
|
positionsConnection,
|
||||||
|
{ telemetryFieldRegistry: geliosTelemetryFieldRegistryV1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION);
|
||||||
|
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}$/);
|
||||||
|
assert.match(positionsPlan.artifacts.mappingContractDigest, /^sha256:[a-f0-9]{64}$/);
|
||||||
|
assert.match(positionsPlan.artifacts.telemetryRegistryDigest, /^sha256:[a-f0-9]{64}$/);
|
||||||
|
assert.deepEqual(positionsPlan.steps.map((step) => step.kind), [
|
||||||
|
"collection_trigger",
|
||||||
|
"provider_request",
|
||||||
|
"provider_request",
|
||||||
|
"extract_items",
|
||||||
|
"semantic_mapping",
|
||||||
|
"data_product_publish",
|
||||||
|
]);
|
||||||
|
const unitsRequest = positionsPlan.steps.find(
|
||||||
|
(step) => step.kind === "provider_request" && step.config.capabilityId === "gelios.units.current.read",
|
||||||
|
);
|
||||||
|
assert.deepEqual(unitsRequest.config.request.query, {
|
||||||
|
incltrip: "true",
|
||||||
|
inclcntrs: "true",
|
||||||
|
inclsnsrs: "true",
|
||||||
|
incllsv: "true",
|
||||||
|
});
|
||||||
|
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"],
|
||||||
|
);
|
||||||
|
assert.equal(Object.isFrozen(positionsPlan), true);
|
||||||
|
assert.equal(Object.isFrozen(positionsPlan.steps), true);
|
||||||
|
|
||||||
|
const clonedPlan = compileL2ExecutionPlan(
|
||||||
|
structuredClone(geliosProviderPackageV8),
|
||||||
|
structuredClone(positionsConnection),
|
||||||
|
{ telemetryFieldRegistry: structuredClone(geliosTelemetryFieldRegistryV1) },
|
||||||
|
);
|
||||||
|
assert.equal(clonedPlan.executionPlanDigest, positionsPlan.executionPlanDigest);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => compileL2ExecutionPlan(geliosProviderPackageV8, positionsConnection),
|
||||||
|
/telemetry_registry_required/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const profileConnection = instantiateL2Connection(geliosProviderPackageV8, {
|
||||||
|
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);
|
||||||
|
assert.deepEqual(validateL2ExecutionPlan(profilePlan), { ok: true, errors: [] });
|
||||||
|
assert.equal(profilePlan.artifacts.telemetryRegistryDigest, undefined);
|
||||||
|
assert.notEqual(profilePlan.executionPlanDigest, positionsPlan.executionPlanDigest);
|
||||||
|
|
||||||
|
const secondConnection = instantiateL2Connection(geliosProviderPackageV8, {
|
||||||
|
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,
|
||||||
|
secondConnection,
|
||||||
|
{ telemetryFieldRegistry: geliosTelemetryFieldRegistryV1 },
|
||||||
|
);
|
||||||
|
assert.notEqual(secondPlan.executionPlanDigest, positionsPlan.executionPlanDigest);
|
||||||
|
|
||||||
|
const receipt = attestL2ExecutionPlanMaterialization(positionsPlan, {
|
||||||
|
graphRevision: "engine-revision-fixture-0001",
|
||||||
|
graphDigest: "a".repeat(64),
|
||||||
|
materializedStepIds: positionsPlan.steps.map((step) => step.id),
|
||||||
|
});
|
||||||
|
assert.equal(receipt.schemaVersion, L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION);
|
||||||
|
assert.equal(receipt.executionPlanDigest, positionsPlan.executionPlanDigest);
|
||||||
|
assert.equal(receipt.graphDigest, `sha256:${"a".repeat(64)}`);
|
||||||
|
assert.match(receipt.receiptDigest, /^sha256:[a-f0-9]{64}$/);
|
||||||
|
assert.equal(Object.isFrozen(receipt), true);
|
||||||
|
|
||||||
|
assert.throws(() => attestL2ExecutionPlanMaterialization(positionsPlan, {
|
||||||
|
graphRevision: "engine-revision-fixture-0001",
|
||||||
|
graphDigest: "a".repeat(64),
|
||||||
|
materializedStepIds: positionsPlan.steps.slice(1).map((step) => step.id),
|
||||||
|
}), /materializedStepIds_must_exactly_match/);
|
||||||
|
|
||||||
|
const tamperedPlan = structuredClone(positionsPlan);
|
||||||
|
tamperedPlan.steps[0].config.mode = "history";
|
||||||
|
assert.equal(
|
||||||
|
validateL2ExecutionPlan(tamperedPlan).errors.includes("executionPlan.executionPlanDigest_mismatch"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(positionsPlan);
|
||||||
|
assert.equal(serialized.includes("Bearer "), false);
|
||||||
|
assert.equal(serialized.includes("access_token"), false);
|
||||||
|
assert.equal(serialized.includes("refresh_token"), false);
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import {
|
||||||
|
TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION,
|
||||||
|
compileTelemetryFieldProjection,
|
||||||
|
digestTelemetryFieldRegistry,
|
||||||
|
validateTelemetryFieldRegistry,
|
||||||
|
} from "../src/index.mjs";
|
||||||
|
import {
|
||||||
|
geliosTelemetryFieldRegistryV1,
|
||||||
|
} from "../providers/gelios/v8/index.mjs";
|
||||||
|
|
||||||
|
assert.deepEqual(validateTelemetryFieldRegistry(geliosTelemetryFieldRegistryV1), {
|
||||||
|
ok: true,
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
assert.match(digestTelemetryFieldRegistry(geliosTelemetryFieldRegistryV1), /^sha256:[a-f0-9]{64}$/);
|
||||||
|
|
||||||
|
const projection = compileTelemetryFieldProjection(geliosTelemetryFieldRegistryV1, {
|
||||||
|
providerPackageId: "gelios.provider.v8",
|
||||||
|
providerPackageVersion: "8.0.0",
|
||||||
|
dataProductId: "fleet.positions.current.v5",
|
||||||
|
surface: "data_product",
|
||||||
|
});
|
||||||
|
assert.equal(projection.schemaVersion, TELEMETRY_FIELD_PROJECTION_SCHEMA_VERSION);
|
||||||
|
assert.deepEqual(projection.entries.map((entry) => entry.sourceKey), ["in0", "in1"]);
|
||||||
|
assert.deepEqual(projection.entries.map((entry) => entry.reading.id), [
|
||||||
|
"sensor.param.in0",
|
||||||
|
"sensor.param.in1",
|
||||||
|
]);
|
||||||
|
assert.equal(projection.entries.some((entry) => entry.sourceKey === "in_0"), false);
|
||||||
|
assert.equal(Object.isFrozen(projection), true);
|
||||||
|
assert.equal(Object.isFrozen(projection.entries), true);
|
||||||
|
|
||||||
|
const observedExposed = structuredClone(geliosTelemetryFieldRegistryV1);
|
||||||
|
observedExposed.entries.find((entry) => entry.sourceKey === "in_0").allowedSurfaces.push("foundry");
|
||||||
|
assert.equal(
|
||||||
|
validateTelemetryFieldRegistry(observedExposed).errors.includes(
|
||||||
|
"registry.entries[2].nonapproved_surface_must_be_audit_only",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const approvedUnclassified = structuredClone(geliosTelemetryFieldRegistryV1);
|
||||||
|
approvedUnclassified.entries[0].sensitivity = "unclassified";
|
||||||
|
assert.equal(
|
||||||
|
validateTelemetryFieldRegistry(approvedUnclassified).errors.includes(
|
||||||
|
"registry.entries[0].approved_must_be_operational",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const wildcardSource = structuredClone(geliosTelemetryFieldRegistryV1);
|
||||||
|
wildcardSource.entries[0].sourceKey = "*";
|
||||||
|
assert.equal(
|
||||||
|
validateTelemetryFieldRegistry(wildcardSource).errors.includes(
|
||||||
|
"registry.entries[0].sourceKey_invalid",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicateReading = structuredClone(geliosTelemetryFieldRegistryV1);
|
||||||
|
duplicateReading.entries[1].semanticReading.id = duplicateReading.entries[0].semanticReading.id;
|
||||||
|
assert.equal(
|
||||||
|
validateTelemetryFieldRegistry(duplicateReading).errors.includes(
|
||||||
|
"registry.entries[1].semanticReading.id_must_be_unique",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(() => compileTelemetryFieldProjection(geliosTelemetryFieldRegistryV1, {
|
||||||
|
providerPackageId: "another.provider.v1",
|
||||||
|
providerPackageVersion: "8.0.0",
|
||||||
|
dataProductId: "fleet.positions.current.v5",
|
||||||
|
surface: "data_product",
|
||||||
|
}), /providerPackageId_mismatch/);
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(geliosTelemetryFieldRegistryV1);
|
||||||
|
for (const forbidden of ["phone", "imei", "commands", "access_token", "refresh_token"]) {
|
||||||
|
assert.equal(serialized.toLowerCase().includes(forbidden), false);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue