feat(foundry): complete HGeoZone live layer

This commit is contained in:
Codex
2026-07-22 09:25:32 +03:00
parent de060c105b
commit 309cddef73
9 changed files with 391 additions and 83 deletions
+3 -2
View File
@@ -17,6 +17,7 @@ import { createFoundryAgentStore } from "./foundry-agent-store.mjs";
import { createFoundryDataProductConsumerManager } from "./foundry-data-product-consumer.mjs";
import { createFoundryReaderGrantProvisioner } from "./foundry-reader-grant-provisioner.mjs";
import { handleFoundryEntitlementRequest, handleFoundryMcpRequest } from "./foundry-mcp.mjs";
import { isMapLiveDataSlot } from "./map-live-data-slot.mjs";
import { normalizeMapPresentationProfile, normalizeMapPresentationProfiles } from "./map-presentation-profile.mjs";
import { createFoundryAuth } from "./nodedc-auth.mjs";
@@ -1049,7 +1050,7 @@ async function resolveRuntimeMapDataProductBinding(applicationId, pageId, bindin
const binding = layout.dataProductBindings.find((candidate) => candidate.id === bindingId);
if (!binding) throw applicationError("data_product_binding_not_found", 404);
const slot = template?.slots?.find((candidate) => candidate.id === binding.slotId);
if (!slot || slot.kind !== "entity-stream") throw applicationError("map_entity_stream_slot_required", 409);
if (!isMapLiveDataSlot(slot)) throw applicationError("map_live_data_slot_required", 409);
return { application, page, binding };
}
@@ -2076,7 +2077,7 @@ const foundryMcpOperations = {
if (page.template?.id !== "map") throw applicationError("map_page_required");
const template = findPageTemplate(page.template.id, page.template.version);
const slot = template?.slots?.find((candidate) => candidate.id === binding.slotId);
if (!slot || slot.kind !== "entity-stream") throw applicationError("map_entity_stream_slot_required");
if (!isMapLiveDataSlot(slot)) throw applicationError("map_live_data_slot_required");
const layout = validateMapPageLayout(page.layout?.map || defaultMapPageLayout());
const existingBinding = layout.dataProductBindings.find((item) => item.id === binding.id);
const resolvedBinding = {
@@ -678,7 +678,7 @@ function zoneV2Target(fieldProjection = zoneV2Projection) {
binding: {
id: "depttrans-pmd-slow-zones",
dataProductId: zoneV2Product.id,
slotId: "points",
slotId: "zones",
delivery: "snapshot+patch",
semanticTypes: ["map.zone"],
fieldProjection: [...fieldProjection],
+2 -2
View File
@@ -608,7 +608,7 @@ const tools = [
{
name: "foundry_upsert_map_data_product_binding",
title: "Upsert Map data product binding",
description: "Bind one approved provider-neutral data product to an entity-stream slot of a Map Page instance. The binding stores no provider transport, endpoint, tenant, connection or credential.",
description: "Bind one approved provider-neutral data product to a live-data slot (points or zones) of a Map Page instance. The binding stores no provider transport, endpoint, tenant, connection or credential.",
inputSchema: {
type: "object",
additionalProperties: false,
@@ -625,7 +625,7 @@ const tools = [
displayName: { type: "string", minLength: 1, maxLength: 120, description: "User-defined text label shown in the Objects menu and window header." },
order: { type: "integer", minimum: 0, maximum: 10000, description: "Application composition order inside the Objects menu." },
dataProductId: { type: "string", description: "Versioned provider-neutral product, for example fleet.positions.current.v1." },
slotId: { type: "string", description: "Approved Map Page entity-stream slot." },
slotId: { type: "string", description: "Approved Map Page live-data slot (entity-stream or zone-stream)." },
semanticTypes: { type: "array", items: { type: "string" } },
fieldProjection: { type: "array", items: { type: "string" } },
presentationProfileId: { type: "string", description: "Existing page-owned provider-neutral Map presentation profile id." },
+11
View File
@@ -0,0 +1,11 @@
const MAP_LIVE_DATA_SLOT_KINDS = new Set(["entity-stream", "zone-stream"]);
/**
* Map runtime bindings may only target slots that deliver canonical live
* entity facts. Point entities and zones have distinct template slots, but
* share the same provider-neutral snapshot/patch transport contract.
*/
export function isMapLiveDataSlot(slot) {
return Boolean(slot && MAP_LIVE_DATA_SLOT_KINDS.has(slot.kind));
}
+15
View File
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isMapLiveDataSlot } from "./map-live-data-slot.mjs";
test("point and zone streams are approved Map live-data slots", () => {
assert.equal(isMapLiveDataSlot({ id: "points", kind: "entity-stream" }), true);
assert.equal(isMapLiveDataSlot({ id: "zones", kind: "zone-stream" }), true);
});
test("non-entity Map slots cannot receive a data-product binding", () => {
for (const kind of ["trace-stream", "route-stream", "selection", "command", "assistant", undefined]) {
assert.equal(isMapLiveDataSlot(kind ? { id: "other", kind } : null), false);
}
});