feat(provider-contract): define materializable L2 graph blueprints
This commit is contained in:
parent
a5a5644dbf
commit
4cf3a83ef1
|
|
@ -52,11 +52,17 @@ Foundry.
|
|||
детерминированный `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.
|
||||
template, mapping, field policy, Data Product и telemetry registry. 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
|
||||
не содержит credential reference и фиксируется отдельным SHA-256 digest.
|
||||
Runtime обязан fail-closed отклонить неизвестный operator, rule или geometry
|
||||
strategy; молча подставлять provider-specific code запрещено. В компиляторе
|
||||
нет веток по provider ID. Provider-specific URL, source paths и параметры
|
||||
являются данными immutable package. После материализации Engine
|
||||
`attestL2ExecutionPlanMaterialization` связывает exact plan/blueprint digest с
|
||||
exact graph revision/digest и точным списком materialized steps.
|
||||
|
||||
Динамическая телеметрия проходит отдельный
|
||||
`nodedc.telemetry-field-registry/v1`. Реестр хранит только имена source
|
||||
|
|
|
|||
|
|
@ -146,18 +146,23 @@ value.mappingContracts.push({
|
|||
observedAt: { strategy: "first_non_empty", paths: ["updatedAt"], coerce: "unix_or_iso_timestamp", fallback: "collection_received_at" },
|
||||
geometry: {
|
||||
type: "GeoJSON",
|
||||
strategy: "gelios_geozone_v1",
|
||||
paths: ["type", "points", "line", "radius"],
|
||||
strategy: "bounded_zone_geometry_v1",
|
||||
kindPath: "type",
|
||||
polygonPointsPath: "points",
|
||||
corridorPointsPath: "line",
|
||||
radiusPath: "radius",
|
||||
coordinateOrder: "latitude_longitude",
|
||||
circleSegments: 64,
|
||||
allowedTypes: ["Polygon", "MultiPolygon"],
|
||||
omitIfInvalid: false,
|
||||
},
|
||||
attributes: {
|
||||
area_square_meters: { strategy: "first_non_empty", paths: ["surfaceArea", "surface_area"], coerce: "number", minimum: 0, omitIfInvalid: true },
|
||||
area_square_meters: { strategy: "first_non_empty", paths: ["surfaceArea", "surface_area"], coerce: "number", minimum: 0, omitIfMissing: true, omitIfInvalid: true },
|
||||
description: { strategy: "first_non_empty", paths: ["description", "descr"], coerce: "string", omitIfMissing: true },
|
||||
display_name: { strategy: "first_non_empty", paths: ["name"], coerce: "string" },
|
||||
geometry_kind: { derive: "geometry_kind" },
|
||||
max_speed_kph: { strategy: "first_non_empty", paths: ["maxPermissibleSpeed", "max_permissible_speed"], coerce: "number", minimum: 0, omitIfInvalid: true },
|
||||
perimeter_meters: { strategy: "first_non_empty", paths: ["perimeter"], coerce: "number", minimum: 0, omitIfInvalid: true },
|
||||
max_speed_kph: { strategy: "first_non_empty", paths: ["maxPermissibleSpeed", "max_permissible_speed"], coerce: "number", minimum: 0, omitIfMissing: true, omitIfInvalid: true },
|
||||
perimeter_meters: { strategy: "first_non_empty", paths: ["perimeter"], coerce: "number", minimum: 0, omitIfMissing: true, omitIfInvalid: true },
|
||||
source_kind: { constant: "live_api" },
|
||||
source_revision: { constant: "gelios-rest-v1" },
|
||||
style_color: { strategy: "first_non_empty", paths: ["color"], coerce: "string", omitIfMissing: true },
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ value.collectionProfiles.push({
|
|||
maxCurrentEntities: 5000,
|
||||
onExceed: "require_partitioned_data_product",
|
||||
},
|
||||
retry: { maxAttempts: 4, backoff: "exponential_with_jitter" },
|
||||
retry: { maxAttempts: 4, backoff: "fixed_delay", delayMs: 2000 },
|
||||
});
|
||||
|
||||
value.dataProducts.push({
|
||||
|
|
@ -232,6 +232,13 @@ value.l2Templates.push({
|
|||
],
|
||||
});
|
||||
|
||||
// The target Engine runtime materializes bounded fixed-delay retries exactly.
|
||||
// v8 therefore avoids promising a backoff algorithm that n8n cannot execute.
|
||||
value.collectionProfiles = value.collectionProfiles.map((profile) => ({
|
||||
...profile,
|
||||
retry: { maxAttempts: 4, backoff: "fixed_delay", delayMs: 2000 },
|
||||
}));
|
||||
|
||||
export const geliosProviderPackageV8 = deepFreeze(value);
|
||||
|
||||
function optional(type) {
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ export {
|
|||
export {
|
||||
L2_EXECUTION_PLAN_COMPILER_VERSION,
|
||||
L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||
L2_GRAPH_BLUEPRINT_SCHEMA_VERSION,
|
||||
L2_MAPPING_RUNTIME_SCHEMA_VERSION,
|
||||
L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
|
||||
attestL2ExecutionPlanMaterialization,
|
||||
compileL2ExecutionPlan,
|
||||
compileL2GraphBlueprint,
|
||||
validateL2ExecutionPlan,
|
||||
} from "./l2-execution-plan.mjs";
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -6,14 +6,23 @@ import {
|
|||
} from "./telemetry-field-registry.mjs";
|
||||
|
||||
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_EXECUTION_PLAN_COMPILER_VERSION = "1.0.0";
|
||||
export const L2_MAPPING_RUNTIME_SCHEMA_VERSION = "nodedc.semantic-mapping-runtime/v1";
|
||||
export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.1.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"]);
|
||||
const STEP_RUNTIME_KINDS = Object.freeze({
|
||||
collection_trigger: "ndc.collection-trigger",
|
||||
provider_request: "ndc.provider-request",
|
||||
extract_items: "ndc.extract-items",
|
||||
semantic_mapping: "ndc.semantic-mapping",
|
||||
data_product_publish: "ndc.data-product-publish",
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolve one immutable provider package plus one connection instance into a
|
||||
|
|
@ -86,6 +95,12 @@ export function compileL2ExecutionPlan(providerPackage, connectionInstance, opti
|
|||
telemetryProjection,
|
||||
artifacts,
|
||||
}));
|
||||
const graphBlueprint = compileGraphBlueprint({
|
||||
profile,
|
||||
template,
|
||||
mapping,
|
||||
steps,
|
||||
});
|
||||
|
||||
const planWithoutDigest = {
|
||||
schemaVersion: L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||
|
|
@ -108,6 +123,7 @@ export function compileL2ExecutionPlan(providerPackage, connectionInstance, opti
|
|||
collectionProfile: structuredClone(profile),
|
||||
artifacts,
|
||||
steps,
|
||||
graphBlueprint,
|
||||
};
|
||||
|
||||
return deepFreeze({
|
||||
|
|
@ -128,6 +144,7 @@ export function validateL2ExecutionPlan(value) {
|
|||
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");
|
||||
}
|
||||
validateGraphBlueprint(value, errors);
|
||||
if (!HASH.test(String(value.executionPlanDigest || ""))) {
|
||||
errors.push("executionPlan.executionPlanDigest_invalid");
|
||||
} else {
|
||||
|
|
@ -140,6 +157,20 @@ export function validateL2ExecutionPlan(value) {
|
|||
return result(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce the exact provider-neutral graph shape required by one resolved
|
||||
* execution plan. Runtime adapters may translate these generic node kinds to
|
||||
* their native graph representation, but may not add provider branches or
|
||||
* silently weaken the declared execution policies.
|
||||
*/
|
||||
export function compileL2GraphBlueprint(executionPlan) {
|
||||
const validation = validateL2ExecutionPlan(executionPlan);
|
||||
if (!validation.ok) {
|
||||
throw new Error(`l2_execution_plan_invalid:${validation.errors.join(",")}`);
|
||||
}
|
||||
return executionPlan.graphBlueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a successful Engine graph materialization back to the exact compiled
|
||||
* plan without teaching this package how Engine stores or renders its graph.
|
||||
|
|
@ -245,6 +276,180 @@ function compileStep(context) {
|
|||
throw new Error(`l2_execution_plan_step_kind_unsupported:${String(step.kind)}`);
|
||||
}
|
||||
|
||||
function compileGraphBlueprint({ profile, template, mapping, steps }) {
|
||||
const nodes = steps.flatMap((step) => {
|
||||
if (step.kind !== "collection_trigger") return [compileBlueprintNode(step, step.id)];
|
||||
if (profile.mode !== "realtime") {
|
||||
return [compileBlueprintNode(step, step.id, "ndc.manual-trigger", { entrypoint: "manual" })];
|
||||
}
|
||||
return [
|
||||
compileBlueprintNode(
|
||||
step,
|
||||
`${step.id}.manual`,
|
||||
"ndc.manual-trigger",
|
||||
{ entrypoint: "manual" },
|
||||
),
|
||||
compileBlueprintNode(
|
||||
step,
|
||||
`${step.id}.interval`,
|
||||
"ndc.interval-trigger",
|
||||
{ entrypoint: "interval", intervalMs: profile.schedule.intervalMs },
|
||||
),
|
||||
];
|
||||
}).map((node, order) => ({ ...node, order }));
|
||||
const triggerNodes = nodes.filter((node) => node.stepId === steps[0].id);
|
||||
const executableNodes = nodes.filter((node) => node.stepId !== steps[0].id);
|
||||
const edgePairs = [
|
||||
...triggerNodes.map((node) => ({ from: node.id, to: executableNodes[0].id })),
|
||||
...executableNodes.slice(1).map((node, index) => ({
|
||||
from: executableNodes[index].id,
|
||||
to: node.id,
|
||||
})),
|
||||
];
|
||||
const edges = edgePairs.map((edge, index) => ({
|
||||
id: `edge.${String(index + 1).padStart(3, "0")}`,
|
||||
...edge,
|
||||
channel: "main",
|
||||
order: 0,
|
||||
}));
|
||||
const blueprintWithoutDigest = {
|
||||
schemaVersion: L2_GRAPH_BLUEPRINT_SCHEMA_VERSION,
|
||||
runtime: {
|
||||
target: template.runtime,
|
||||
instanceMode: template.instanceMode,
|
||||
executionMode: profile.mode,
|
||||
entrypoints: profile.mode === "realtime"
|
||||
? [
|
||||
{ kind: "manual", purpose: "managed_deploy_and_run" },
|
||||
{ kind: "interval", intervalMs: profile.schedule.intervalMs, purpose: "scheduled_collection" },
|
||||
]
|
||||
: [{ kind: "manual", purpose: "managed_deploy_and_run" }],
|
||||
envelope: {
|
||||
schemaVersion: "nodedc.l2-runtime-envelope/v1",
|
||||
providerResponses: "keyed_by_capability_id",
|
||||
collectionReceivedAt: "required_iso_timestamp",
|
||||
canonicalFactsOnlyAtPublish: true,
|
||||
rawProviderPayloadAtPublish: "forbidden",
|
||||
maxProviderResponseBytes: 16 * 1024 * 1024,
|
||||
maxExtractedItems: profile.cardinality.maxCurrentEntities,
|
||||
},
|
||||
retry: structuredClone(profile.retry),
|
||||
batching: structuredClone(profile.batching),
|
||||
cardinality: structuredClone(profile.cardinality),
|
||||
failureMode: "fail_closed",
|
||||
mappingRuntime: compileMappingRuntimeRequirements(mapping),
|
||||
},
|
||||
nodes,
|
||||
edges,
|
||||
};
|
||||
return {
|
||||
...blueprintWithoutDigest,
|
||||
blueprintDigest: canonicalDigest(blueprintWithoutDigest),
|
||||
};
|
||||
}
|
||||
|
||||
function compileBlueprintNode(step, id, runtimeKind = STEP_RUNTIME_KINDS[step.kind], adapterConfig) {
|
||||
return {
|
||||
id,
|
||||
stepId: step.id,
|
||||
runtimeKind,
|
||||
configDigest: canonicalDigest({
|
||||
stepConfig: step.config,
|
||||
...(adapterConfig ? { adapterConfig } : {}),
|
||||
}),
|
||||
...(adapterConfig ? { adapterConfig } : {}),
|
||||
credentialSlots: step.config?.credentialBinding
|
||||
? [{
|
||||
role: step.config.credentialBinding,
|
||||
slot: step.config.credentialBinding,
|
||||
authority: "engine_managed_opaque_ref",
|
||||
}]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function compileMappingRuntimeRequirements(mapping) {
|
||||
const expressions = [];
|
||||
collectMappingExpressions(mapping.fact, expressions);
|
||||
const geometry = mapping.fact?.geometry;
|
||||
return {
|
||||
schemaVersion: L2_MAPPING_RUNTIME_SCHEMA_VERSION,
|
||||
expressionSources: uniqueSorted(expressions.map(expressionSourceKind)),
|
||||
coercions: uniqueSorted(expressions.map((expression) => expression.coerce).filter(Boolean)),
|
||||
derivationKinds: uniqueSorted(Object.values(mapping.derivations || {}).map((item) => item.kind)),
|
||||
ruleIds: uniqueSorted(
|
||||
Object.values(mapping.derivations || {}).flatMap((item) => item.rules || []),
|
||||
),
|
||||
geometryStrategies: geometry
|
||||
? [geometry.type === "Point" ? "point_from_longitude_latitude" : geometry.strategy]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function collectMappingExpressions(value, found) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => collectMappingExpressions(item, found));
|
||||
return;
|
||||
}
|
||||
if (!isPlainObject(value)) return;
|
||||
if (Array.isArray(value.paths) || value.constant !== undefined || typeof value.derive === "string") {
|
||||
found.push(value);
|
||||
}
|
||||
Object.values(value).forEach((item) => collectMappingExpressions(item, found));
|
||||
}
|
||||
|
||||
function expressionSourceKind(value) {
|
||||
if (Array.isArray(value.paths)) return value.strategy || "first_non_empty";
|
||||
if (value.constant !== undefined) return "constant";
|
||||
return "derive";
|
||||
}
|
||||
|
||||
function validateGraphBlueprint(executionPlan, errors) {
|
||||
if (!isPlainObject(executionPlan.graphBlueprint)) {
|
||||
errors.push("executionPlan.graphBlueprint_must_be_object");
|
||||
return;
|
||||
}
|
||||
const blueprint = executionPlan.graphBlueprint;
|
||||
if (blueprint.schemaVersion !== L2_GRAPH_BLUEPRINT_SCHEMA_VERSION) {
|
||||
errors.push("executionPlan.graphBlueprint.schemaVersion_mismatch");
|
||||
}
|
||||
if (!HASH.test(String(blueprint.blueprintDigest || ""))) {
|
||||
errors.push("executionPlan.graphBlueprint.blueprintDigest_invalid");
|
||||
} else {
|
||||
const descriptor = structuredClone(blueprint);
|
||||
delete descriptor.blueprintDigest;
|
||||
if (normalizeDigest(blueprint.blueprintDigest) !== canonicalDigest(descriptor)) {
|
||||
errors.push("executionPlan.graphBlueprint.blueprintDigest_mismatch");
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(executionPlan.steps) || !isPlainObject(executionPlan.collectionProfile)) return;
|
||||
try {
|
||||
const expected = compileGraphBlueprintDescriptor({
|
||||
profile: executionPlan.collectionProfile,
|
||||
steps: executionPlan.steps,
|
||||
mapping: executionPlan.steps.find((step) => step?.kind === "semantic_mapping")?.config?.mappingContract,
|
||||
});
|
||||
if (stableJson(blueprint) !== stableJson(expected)) {
|
||||
errors.push("executionPlan.graphBlueprint_mismatch");
|
||||
}
|
||||
} catch {
|
||||
errors.push("executionPlan.graphBlueprint_unverifiable");
|
||||
}
|
||||
}
|
||||
|
||||
function compileGraphBlueprintDescriptor({ profile, steps, mapping }) {
|
||||
if (!isPlainObject(mapping)) throw new Error("graph_blueprint_context_invalid");
|
||||
const template = {
|
||||
runtime: "ndc_l2",
|
||||
instanceMode: "one_connection_per_workflow",
|
||||
};
|
||||
return compileGraphBlueprint({ profile, template, mapping, steps });
|
||||
}
|
||||
|
||||
function uniqueSorted(values) {
|
||||
return [...new Set(values)].sort();
|
||||
}
|
||||
|
||||
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") {
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@ const EXPRESSION_KEYS = new Set([
|
|||
]);
|
||||
const GEOMETRY_KEYS = new Set([
|
||||
"type", "longitude", "latitude", "strategy", "paths", "allowedTypes", "omitIfInvalid",
|
||||
"kindPath", "polygonPointsPath", "corridorPointsPath", "radiusPath",
|
||||
"coordinateOrder", "circleSegments",
|
||||
]);
|
||||
const DERIVATION_KEYS = new Set(["kind", "rules", "default", "parameters"]);
|
||||
const TEMPLATE_KEYS = new Set([
|
||||
|
|
@ -593,9 +595,18 @@ function validateCollectionProfileTemplate(value, path, errors) {
|
|||
if (!isPlainObject(value.retry)) {
|
||||
errors.push(`${path}.retry_must_be_object`);
|
||||
} else {
|
||||
rejectUnknownKeys(value.retry, new Set(["maxAttempts", "backoff"]), `${path}.retry`, errors);
|
||||
rejectUnknownKeys(value.retry, new Set(["maxAttempts", "backoff", "delayMs"]), `${path}.retry`, errors);
|
||||
if (!Number.isInteger(value.retry.maxAttempts) || value.retry.maxAttempts < 1 || value.retry.maxAttempts > 10) errors.push(`${path}.retry.maxAttempts_invalid`);
|
||||
if (value.retry.backoff !== "exponential_with_jitter") errors.push(`${path}.retry.backoff_invalid`);
|
||||
if (!["exponential_with_jitter", "fixed_delay"].includes(value.retry.backoff)) {
|
||||
errors.push(`${path}.retry.backoff_invalid`);
|
||||
}
|
||||
if (value.retry.backoff === "fixed_delay") {
|
||||
if (!Number.isInteger(value.retry.delayMs) || value.retry.delayMs < 100 || value.retry.delayMs > 60_000) {
|
||||
errors.push(`${path}.retry.delayMs_invalid`);
|
||||
}
|
||||
} else if (value.retry.delayMs !== undefined) {
|
||||
errors.push(`${path}.retry.delayMs_requires_fixed_delay`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -672,13 +683,29 @@ function validateMappingContract(value, path, errors) {
|
|||
errors.push(`${path}.fact.geometry.point_shape_invalid`);
|
||||
}
|
||||
} else if (value.fact.geometry.type === "GeoJSON") {
|
||||
if (value.fact.geometry.strategy !== "gelios_geozone_v1") errors.push(`${path}.fact.geometry.strategy_invalid`);
|
||||
requiredSourcePathArray(value.fact.geometry.paths, `${path}.fact.geometry.paths`, errors);
|
||||
if (value.fact.geometry.strategy !== "bounded_zone_geometry_v1") errors.push(`${path}.fact.geometry.strategy_invalid`);
|
||||
for (const key of ["kindPath", "polygonPointsPath", "corridorPointsPath", "radiusPath"]) {
|
||||
if (!isCanonicalSourcePath(value.fact.geometry[key])) {
|
||||
errors.push(`${path}.fact.geometry.${key}_invalid`);
|
||||
}
|
||||
}
|
||||
if (!["latitude_longitude", "longitude_latitude"].includes(value.fact.geometry.coordinateOrder)) {
|
||||
errors.push(`${path}.fact.geometry.coordinateOrder_invalid`);
|
||||
}
|
||||
if (!Number.isInteger(value.fact.geometry.circleSegments)
|
||||
|| value.fact.geometry.circleSegments < 16
|
||||
|| value.fact.geometry.circleSegments > 256) {
|
||||
errors.push(`${path}.fact.geometry.circleSegments_invalid`);
|
||||
}
|
||||
if (!sameSet(value.fact.geometry.allowedTypes, ["Polygon", "MultiPolygon"])) {
|
||||
errors.push(`${path}.fact.geometry.allowedTypes_must_be_polygon_or_multipolygon`);
|
||||
}
|
||||
if (value.fact.geometry.omitIfInvalid !== false) errors.push(`${path}.fact.geometry.omitIfInvalid_must_be_false`);
|
||||
if (value.fact.geometry.longitude !== undefined || value.fact.geometry.latitude !== undefined) {
|
||||
if (
|
||||
value.fact.geometry.longitude !== undefined
|
||||
|| value.fact.geometry.latitude !== undefined
|
||||
|| value.fact.geometry.paths !== undefined
|
||||
) {
|
||||
errors.push(`${path}.fact.geometry.geojson_shape_invalid`);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -900,6 +927,9 @@ function validateExpressionAgainstFieldContract(expression, contract, derivation
|
|||
errors.push(`${path}_required_but_mapping_can_omit_invalid`);
|
||||
}
|
||||
if (expression.paths !== undefined) {
|
||||
if (contract.required === false && expression.omitIfMissing !== true) {
|
||||
errors.push(`${path}_optional_source_field_must_omit_missing`);
|
||||
}
|
||||
const expectedCoerce = new Map([["string", "string"], ["number", "number"], ["boolean", "boolean"]]).get(contract.type);
|
||||
if (expectedCoerce && expression.coerce !== expectedCoerce) errors.push(`${path}.coerce_must_match_field_contract`);
|
||||
if (Array.isArray(contract.enum)) errors.push(`${path}.enum_field_must_use_constant_or_derivation`);
|
||||
|
|
@ -1041,8 +1071,11 @@ function mappingSourcePaths(value, found = []) {
|
|||
if (isCanonicalSourcePath(path)) found.push(path);
|
||||
});
|
||||
}
|
||||
for (const key of ["kindPath", "polygonPointsPath", "corridorPointsPath", "radiusPath"]) {
|
||||
if (isCanonicalSourcePath(value[key])) found.push(value[key]);
|
||||
}
|
||||
Object.entries(value).forEach(([key, item]) => {
|
||||
if (key !== "paths") mappingSourcePaths(item, found);
|
||||
if (key !== "paths" && !key.endsWith("Path")) mappingSourcePaths(item, found);
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
L2_EXECUTION_PLAN_SCHEMA_VERSION,
|
||||
L2_GRAPH_BLUEPRINT_SCHEMA_VERSION,
|
||||
L2_MAPPING_RUNTIME_SCHEMA_VERSION,
|
||||
L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
|
||||
attestL2ExecutionPlanMaterialization,
|
||||
compileL2ExecutionPlan,
|
||||
compileL2GraphBlueprint,
|
||||
instantiateL2Connection,
|
||||
validateL2ExecutionPlan,
|
||||
} from "../src/index.mjs";
|
||||
|
|
@ -30,6 +34,58 @@ 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.equal(positionsPlan.graphBlueprint.schemaVersion, L2_GRAPH_BLUEPRINT_SCHEMA_VERSION);
|
||||
assert.equal(
|
||||
positionsPlan.graphBlueprint.runtime.mappingRuntime.schemaVersion,
|
||||
L2_MAPPING_RUNTIME_SCHEMA_VERSION,
|
||||
);
|
||||
assert.match(positionsPlan.graphBlueprint.blueprintDigest, /^sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(compileL2GraphBlueprint(positionsPlan), positionsPlan.graphBlueprint);
|
||||
assert.deepEqual(
|
||||
positionsPlan.graphBlueprint.nodes.map(({ id, stepId, runtimeKind, order }) => ({
|
||||
id,
|
||||
stepId,
|
||||
runtimeKind,
|
||||
order,
|
||||
})),
|
||||
[
|
||||
{ id: "collection.trigger.manual", stepId: "collection.trigger", runtimeKind: "ndc.manual-trigger", order: 0 },
|
||||
{ id: "collection.trigger.interval", stepId: "collection.trigger", runtimeKind: "ndc.interval-trigger", order: 1 },
|
||||
{ id: "provider.fetch-monitoring-config", stepId: "provider.fetch-monitoring-config", runtimeKind: "ndc.provider-request", order: 2 },
|
||||
{ id: "provider.fetch-units", stepId: "provider.fetch-units", runtimeKind: "ndc.provider-request", order: 3 },
|
||||
{ id: "provider.extract-units", stepId: "provider.extract-units", runtimeKind: "ndc.extract-items", order: 4 },
|
||||
{ id: "ontology.map", stepId: "ontology.map", runtimeKind: "ndc.semantic-mapping", order: 5 },
|
||||
{ id: "data-product.publish", stepId: "data-product.publish", runtimeKind: "ndc.data-product-publish", order: 6 },
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
positionsPlan.graphBlueprint.edges.map(({ from, to }) => ({ from, to })),
|
||||
[
|
||||
{ from: "collection.trigger.manual", to: "provider.fetch-monitoring-config" },
|
||||
{ from: "collection.trigger.interval", to: "provider.fetch-monitoring-config" },
|
||||
{ from: "provider.fetch-monitoring-config", to: "provider.fetch-units" },
|
||||
{ from: "provider.fetch-units", to: "provider.extract-units" },
|
||||
{ from: "provider.extract-units", to: "ontology.map" },
|
||||
{ from: "ontology.map", to: "data-product.publish" },
|
||||
],
|
||||
);
|
||||
assert.deepEqual(positionsPlan.graphBlueprint.runtime.mappingRuntime.derivationKinds, [
|
||||
"bounded_readings",
|
||||
"ordered_rules",
|
||||
]);
|
||||
assert.deepEqual(positionsPlan.graphBlueprint.runtime.retry, {
|
||||
maxAttempts: 4,
|
||||
backoff: "fixed_delay",
|
||||
delayMs: 2000,
|
||||
});
|
||||
assert.equal(
|
||||
JSON.stringify(positionsPlan.graphBlueprint).includes("ndc-credref:"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
positionsPlan.graphBlueprint.nodes.every((node) => !node.runtimeKind.includes("gelios")),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(positionsPlan.steps.map((step) => step.kind), [
|
||||
"collection_trigger",
|
||||
"provider_request",
|
||||
|
|
@ -54,6 +110,7 @@ assert.deepEqual(
|
|||
);
|
||||
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),
|
||||
|
|
@ -77,6 +134,28 @@ const profilePlan = compileL2ExecutionPlan(geliosProviderPackageV8, profileConne
|
|||
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, {
|
||||
tenantId: "tenant-all-profiles-fixture",
|
||||
connectionId: `connection-${collectionProfile.id.replaceAll(".", "-")}`,
|
||||
collectionProfileId: collectionProfile.id,
|
||||
providerCredentialRef: "ndc-credref:provider-all-profiles-fixture",
|
||||
});
|
||||
const plan = compileL2ExecutionPlan(
|
||||
geliosProviderPackageV8,
|
||||
connection,
|
||||
collectionProfile.dataProductId === "fleet.positions.current.v5"
|
||||
? { telemetryFieldRegistry: geliosTelemetryFieldRegistryV1 }
|
||||
: {},
|
||||
);
|
||||
assert.deepEqual(validateL2ExecutionPlan(plan), { ok: true, errors: [] });
|
||||
assert.equal(JSON.stringify(plan.graphBlueprint.runtime.mappingRuntime).includes("gelios"), false);
|
||||
const expectedEntrypoints = collectionProfile.mode === "realtime" ? 2 : 1;
|
||||
assert.equal(plan.graphBlueprint.nodes.length, plan.steps.length + expectedEntrypoints - 1);
|
||||
assert.equal(plan.graphBlueprint.edges.length, plan.steps.length + expectedEntrypoints - 2);
|
||||
}
|
||||
|
||||
const secondConnection = instantiateL2Connection(geliosProviderPackageV8, {
|
||||
tenantId: "tenant-compiler-fixture",
|
||||
|
|
@ -115,7 +194,39 @@ assert.equal(
|
|||
true,
|
||||
);
|
||||
|
||||
const authorityEscalatedPlan = structuredClone(positionsPlan);
|
||||
authorityEscalatedPlan.graphBlueprint.runtime.target = "provider_specific_runtime";
|
||||
authorityEscalatedPlan.graphBlueprint.blueprintDigest = digestWithout(
|
||||
authorityEscalatedPlan.graphBlueprint,
|
||||
"blueprintDigest",
|
||||
);
|
||||
authorityEscalatedPlan.executionPlanDigest = digestWithout(
|
||||
authorityEscalatedPlan,
|
||||
"executionPlanDigest",
|
||||
);
|
||||
assert.equal(
|
||||
validateL2ExecutionPlan(authorityEscalatedPlan).errors.includes("executionPlan.graphBlueprint_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);
|
||||
|
||||
function digestWithout(value, key) {
|
||||
const descriptor = structuredClone(value);
|
||||
delete descriptor[key];
|
||||
return `sha256:${createHash("sha256").update(JSON.stringify(stableValue(descriptor)), "utf8").digest("hex")}`;
|
||||
}
|
||||
|
||||
function stableValue(value) {
|
||||
if (Array.isArray(value)) return value.map(stableValue);
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return Object.fromEntries(
|
||||
Object.keys(value)
|
||||
.filter((key) => value[key] !== undefined)
|
||||
.sort()
|
||||
.map((key) => [key, stableValue(value[key])]),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue