feat(provider-contract): define materializable L2 graph blueprints

This commit is contained in:
Codex
2026-07-23 20:22:22 +03:00
parent a5a5644dbf
commit 4cf3a83ef1
7 changed files with 388 additions and 18 deletions
@@ -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;
}