911 lines
35 KiB
JavaScript
911 lines
35 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createRequire } from "node:module";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdir } from "node:fs/promises";
|
|
import { existsSync, statSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(scriptDir, "..");
|
|
const projectsRoot = resolve(repoRoot, "..");
|
|
const defaultStorePath = resolve(repoRoot, ".bifrost", "provider-advertisement.cc");
|
|
|
|
const cultCacheRequire = createRequire(resolve(projectsRoot, "CultCacheTS", "package.json"));
|
|
const cultCacheRuntime = loadCultCacheRuntime();
|
|
const {
|
|
CultCache,
|
|
SingleFileMessagePackBackingStore,
|
|
defineDocumentType,
|
|
} = cultCacheRuntime;
|
|
|
|
function loadCultCacheRuntime() {
|
|
const candidates = [
|
|
resolve(projectsRoot, "CultCacheTS", "dist", "index.js"),
|
|
resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "dist", "index.js"),
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
try {
|
|
const runtime = cultCacheRequire(candidate);
|
|
if (runtime.CultCache && runtime.SingleFileMessagePackBackingStore && runtime.defineDocumentType) {
|
|
return runtime;
|
|
}
|
|
} catch {
|
|
// Try the next local CultCache runtime candidate.
|
|
}
|
|
}
|
|
|
|
throw new Error("CultCache TypeScript runtime with document APIs is unavailable.");
|
|
}
|
|
|
|
const documentType = "gamecult.eve.provider_advertisement";
|
|
const schemaId = "gamecult.eve.provider_advertisement.v1";
|
|
const documentId = "bifrost";
|
|
const surfaceDocumentType = "gamecult.eve.surface_state";
|
|
const surfaceSchemaId = "gamecult.eve.surface_state.v1";
|
|
const interfaceBindingDocumentType = "gamecult.eve.interface_binding";
|
|
const interfaceBindingSchemaId = "gamecult.eve.interface_binding.v1";
|
|
const verseId = "bifrost.local";
|
|
const rootVerse = "asgard";
|
|
const currentMachine = "starfire";
|
|
const canonicalService = `${rootVerse}.bifrost`;
|
|
const locatedService = `${rootVerse}.${currentMachine}.bifrost`;
|
|
const plannedLocatedService = `${rootVerse}.yggdrasil.bifrost`;
|
|
|
|
const advertisementDefinition = defineDocumentType({
|
|
type: documentType,
|
|
schemaId,
|
|
schemaName: documentType,
|
|
schemaVersion: schemaId,
|
|
schema: {
|
|
parse: parseAdvertisement,
|
|
},
|
|
name: "providerId",
|
|
indexes: {
|
|
serviceName: "serviceName",
|
|
},
|
|
members: [
|
|
{ slot: 1, memberName: "providerId", typeName: "string", isName: true },
|
|
{ slot: 2, memberName: "serviceName", typeName: "string", indexAlias: "serviceName" },
|
|
{ slot: 3, memberName: "contractPath", typeName: "string" },
|
|
{ slot: 4, memberName: "generatedAt", typeName: "string" },
|
|
{ slot: 5, memberName: "authority", typeName: "object" },
|
|
{ slot: 6, memberName: "namespaces", typeName: "object", isMany: true },
|
|
{ slot: 7, memberName: "schemas", typeName: "object", isMany: true },
|
|
{ slot: 8, memberName: "witnesses", typeName: "object", isMany: true },
|
|
{ slot: 9, memberName: "surfaces", typeName: "object", isMany: true },
|
|
{ slot: 10, memberName: "commandBoundaries", typeName: "object", isMany: true },
|
|
{ slot: 11, memberName: "styleCapabilities", typeName: "object", isMany: true },
|
|
{ slot: 12, memberName: "demotions", typeName: "string", isMany: true },
|
|
{ slot: 13, memberName: "rootVerse", typeName: "string" },
|
|
{ slot: 14, memberName: "canonicalService", typeName: "string" },
|
|
{ slot: 15, memberName: "locatedService", typeName: "string" },
|
|
{ slot: 16, memberName: "cultMeshAddress", typeName: "string" },
|
|
{ slot: 17, memberName: "endpoints", typeName: "object", isMany: true },
|
|
{ slot: 18, memberName: "routes", typeName: "object", isMany: true },
|
|
],
|
|
});
|
|
|
|
const surfaceDefinition = defineDocumentType({
|
|
type: surfaceDocumentType,
|
|
schemaId: surfaceSchemaId,
|
|
schemaName: surfaceDocumentType,
|
|
schemaVersion: surfaceSchemaId,
|
|
schema: { parse: parseObjectDocument("Eve surface state") },
|
|
name: "providerId",
|
|
members: [
|
|
{ slot: 0, memberName: "providerId", typeName: "string", isName: true },
|
|
{ slot: 1, memberName: "title", typeName: "string" },
|
|
{ slot: 2, memberName: "version", typeName: "long" },
|
|
{ slot: 3, memberName: "updatedAt", typeName: "string" },
|
|
{ slot: 4, memberName: "surface", typeName: "object" },
|
|
],
|
|
});
|
|
|
|
const interfaceBindingDefinition = defineDocumentType({
|
|
type: interfaceBindingDocumentType,
|
|
schemaId: interfaceBindingSchemaId,
|
|
schemaName: interfaceBindingDocumentType,
|
|
schemaVersion: interfaceBindingSchemaId,
|
|
schema: { parse: parseObjectDocument("Eve interface binding") },
|
|
name: "bindingId",
|
|
});
|
|
|
|
async function main() {
|
|
const [command, ...rawArgs] = process.argv.slice(2);
|
|
const options = parseArgs(rawArgs);
|
|
|
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
switch (command) {
|
|
case "export":
|
|
await exportAdvertisement(options);
|
|
return;
|
|
case "print":
|
|
printJson(buildAdvertisement(options));
|
|
return;
|
|
case "schema":
|
|
printJson({
|
|
documentType,
|
|
schemaId,
|
|
documentId,
|
|
members: advertisementDefinition.members,
|
|
});
|
|
return;
|
|
default:
|
|
throw new Error(`Unknown command "${command}". Run "node tools/provider-advertisement.mjs help".`);
|
|
}
|
|
}
|
|
|
|
async function exportAdvertisement(options) {
|
|
const storePath = resolveOptionPath(options.out ?? options.store ?? defaultStorePath);
|
|
await mkdir(dirname(storePath), { recursive: true });
|
|
|
|
const cache = CultCache.builder()
|
|
.withDocumentType(advertisementDefinition)
|
|
.withDocumentType(surfaceDefinition)
|
|
.withDocumentType(interfaceBindingDefinition)
|
|
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
|
|
.build();
|
|
|
|
await cache.pullAllBackingStores();
|
|
const advertisement = buildAdvertisement(options);
|
|
const stats = await collectStats(options);
|
|
const surface = buildOperatorSurface(stats, options);
|
|
const binding = buildInterfaceBinding(surface, stats, options);
|
|
await cache.put(advertisementDefinition, advertisement.providerId, advertisement);
|
|
await cache.put(surfaceDefinition, surface.providerId, surface);
|
|
await cache.put(interfaceBindingDefinition, binding.bindingId, binding);
|
|
|
|
printJson({
|
|
ok: true,
|
|
documentType,
|
|
schemaId,
|
|
providerId: advertisement.providerId,
|
|
out: storePath,
|
|
surfaces: advertisement.surfaces.map((surface) => surface.id),
|
|
witnesses: advertisement.witnesses.map((witness) => witness.path),
|
|
stats: stats.summary,
|
|
});
|
|
}
|
|
|
|
function buildAdvertisement(options) {
|
|
return parseAdvertisement({
|
|
providerId: "bifrost",
|
|
serviceName: "Bifrost",
|
|
contractPath: "docs/verse-service-contract.md",
|
|
generatedAt: options["generated-at"] ?? new Date().toISOString(),
|
|
rootVerse,
|
|
canonicalService,
|
|
locatedService,
|
|
plannedLocatedService,
|
|
cultMeshAddress: locatedService,
|
|
endpoints: [
|
|
endpoint("operator-tui", `${locatedService}/eve/tui`, "gamecult.eve.surface.v1", ["tui", "nightwing-tui"]),
|
|
endpoint("operator-gui", `${locatedService}/eve/gui`, "gamecult.eve.surface.v1", ["gui", "browser", "eve-native"]),
|
|
endpoint("operator-commands", `${locatedService}/commands`, "bifrost.bridge_action.v0", ["command"]),
|
|
endpoint("motion-surface", "https://bifrost.gamecult.org/eve/governance/surface", "gamecult.eve.surface.v1", ["product", "governance", "motion"]),
|
|
endpoint("motion-commands", "https://bifrost.gamecult.org/eve/governance/commands", "bifrost.motion_command.v0", ["command", "governance", "motion"]),
|
|
],
|
|
routes: [
|
|
route("cultcache-witness", ".bifrost/provider-advertisement.cc", "local-cultcache", true),
|
|
route("websocket-bridge", "ws://192.168.1.66:8797/eve/deck", "compatibility-eve-deck", true),
|
|
],
|
|
authority: {
|
|
owner: "Bifrost",
|
|
role: "GameCult labor, governance, patron pressure, project work, account membership, and governed-public-crossing provider",
|
|
presentationOwner: "Eve/CultUI",
|
|
discoveryOwner: "Odin through CultMesh",
|
|
stateOwner: "Bifrost typed state with CultCache .cc witnesses or export paths",
|
|
runtimeMigration: `currently ${locatedService}; planned move target ${plannedLocatedService}`,
|
|
},
|
|
namespaces: [
|
|
namespace("gamecult.bifrost.service", "service registration, build/version, schema catalog, and command discovery"),
|
|
namespace("gamecult.bifrost.governance", "motions, topic threads, comments, votes, approvals, objections, and policy receipts"),
|
|
namespace("gamecult.bifrost.work", "projects, work items, claims, review state, completion artifacts, and maintainer acceptance"),
|
|
namespace("gamecult.bifrost.economics", "patron pressure, contributor credit, ledger entries, payout proposal batches, and revenue-share inputs"),
|
|
namespace("gamecult.bifrost.bridge", "GitHub, Discord, Reddit, CultNet/CultCache, and future collaboration crossings plus receipts"),
|
|
namespace("gamecult.bifrost.surface.product", "member, patron, contributor, and project-facing Eve product surfaces"),
|
|
namespace("gamecult.bifrost.surface.operator", "readiness, witness, bridge, schema, migration, and deploy operator surfaces"),
|
|
],
|
|
schemas: [
|
|
schema(schemaId, documentType, "this provider advertisement"),
|
|
schema("gamecult.eve.surface.v1", "gamecult.eve.surface", "Eve/CultUI surface compositions lowered by Eve runtimes"),
|
|
schema("bifrost.governance.topic.v0", "bifrost.governance.topic", "existing CultCache governance topic witness"),
|
|
schema("bifrost.governance.topic_comment.v0", "bifrost.governance.topic-comment", "existing CultCache governance comment witness"),
|
|
schema("bifrost.agent-transport.update-request.v0", "bifrost.agent-transport.update-request", "existing CultCache/CultNet agent intake witness"),
|
|
schema("bifrost.work_item.v0", "bifrost.work-item", "planned Postgres-to-CultCache work item witness"),
|
|
schema("bifrost.motion.v0", "bifrost.motion", "app-native motion witness"),
|
|
schema("bifrost.vote.v0", "bifrost.vote", "motion vote witness"),
|
|
schema("bifrost.motion_command.v0", "bifrost.motion-command", "Eve Motion Verse command envelope for create, vote, and close"),
|
|
schema("bifrost.ledger_entry.v0", "bifrost.ledger-entry", "planned contributor/patron ledger witness"),
|
|
schema("bifrost.bridge_action.v0", "bifrost.bridge-action", "planned governed crossing command witness"),
|
|
schema("bifrost.bridge_receipt.v0", "bifrost.bridge-receipt", "planned governed crossing result witness"),
|
|
schema("bifrost.member_capability_snapshot.v0", "bifrost.member-capability-snapshot", "planned Heimdall-consumed membership capability witness"),
|
|
],
|
|
witnesses: [
|
|
witness(".bifrost/provider-advertisement.cc", schemaId, "current", "read-only provider advertisement exported by this tool"),
|
|
witness(".bifrost/governance-threads.cc", "bifrost.governance.topic.v0; bifrost.governance.topic_comment.v0", "current", "governance discussion, approvals, and dispatch promotion topics"),
|
|
witness(".bifrost/agent-transport.cc", "bifrost.agent-transport.update-request.v0", "current", "repo Persona update requests and dispatch queue state"),
|
|
witness(".bifrost/work-items.cc", "bifrost.work_item.v0", "planned-export", "work items exported from the alpha transactional store"),
|
|
witness(".bifrost/motions.cc", "bifrost.motion.v0; bifrost.vote.v0", "planned-export", "app-native motions and votes exported from the alpha transactional store"),
|
|
witness(".bifrost/ledger.cc", "bifrost.ledger_entry.v0", "planned-export", "patron and contributor ledger entries exported from the alpha transactional store"),
|
|
witness(".bifrost/member-capabilities.cc", "bifrost.member_capability_snapshot.v0", "planned-export", "membership and account capability snapshots consumed by Bifrost"),
|
|
witness(".bifrost/bridge-receipts.cc", "bifrost.bridge_action.v0; bifrost.bridge_receipt.v0", "planned-export", "governed public crossing actions and receipts"),
|
|
witness(".bifrost/eve-surfaces.cc", "gamecult.eve.surface.v1", "planned-export", "product and operator Eve/CultUI surface publications"),
|
|
],
|
|
surfaces: [
|
|
surface("bifrost", "Bifrost Operator Dashboard", "gamecult.bifrost.surface.operator", "gamecult.eve.surface_state.v1", ".bifrost/provider-advertisement.cc", "eve", [
|
|
"service health",
|
|
"compact service status",
|
|
"topic and request status",
|
|
"dispatch activity by source channel",
|
|
"bridge capability status",
|
|
]),
|
|
surface("bifrost.account", "Account Verse", "gamecult.bifrost.surface.product", "gamecult.eve.surface.v1", ".bifrost/eve-surfaces.cc", "account/eve", [
|
|
"membership status",
|
|
"Heimdall-linked account projection",
|
|
"grant consumption",
|
|
"audit trail lowerings",
|
|
]),
|
|
surface("bifrost.patron", "Patron Verse", "gamecult.bifrost.surface.product", "gamecult.eve.surface.v1", ".bifrost/eve-surfaces.cc", "patron/eve", [
|
|
"patron standing",
|
|
"priority pressure",
|
|
"pledge/reward influence",
|
|
"receipts",
|
|
]),
|
|
surface("bifrost.project", "Project Verse", "gamecult.bifrost.surface.product", "gamecult.eve.surface.v1", ".bifrost/eve-surfaces.cc", "project/eve", [
|
|
"project membership",
|
|
"repository links",
|
|
"maintainer authority",
|
|
"work boards",
|
|
]),
|
|
surface("bifrost.work", "Work Verse", "gamecult.bifrost.surface.product", "gamecult.eve.surface.v1", ".bifrost/eve-surfaces.cc", "work/eve", [
|
|
"work items",
|
|
"claims",
|
|
"review",
|
|
"blockers",
|
|
"completion artifacts",
|
|
]),
|
|
surface("bifrost.motion", "Motion Verse", "gamecult.bifrost.surface.product", "gamecult.eve.surface.v1", "/eve/governance/surface", "motion/eve", [
|
|
"motions",
|
|
"topic threads",
|
|
"votes",
|
|
"approvals",
|
|
"objections",
|
|
]),
|
|
surface("bifrost.operator", "Bifrost Operator Verse", "gamecult.bifrost.surface.operator", "gamecult.eve.surface.v1", ".bifrost/eve-surfaces.cc", "operator/eve", [
|
|
"readiness",
|
|
"store freshness",
|
|
"bridge queues",
|
|
"schema catalog",
|
|
"migration drift",
|
|
]),
|
|
],
|
|
commandBoundaries: [
|
|
boundary("work", "Bifrost", [
|
|
"claim work",
|
|
"submit completion",
|
|
"record blockers",
|
|
"maintainer accept/reject",
|
|
], [
|
|
"does not execute payout",
|
|
"does not let GitHub issue state override Bifrost work authority",
|
|
]),
|
|
boundary("motion", "Bifrost", [
|
|
"open motion",
|
|
"comment",
|
|
"vote",
|
|
"approve",
|
|
"object",
|
|
"promote to dispatch",
|
|
], [
|
|
"Discord and Reddit mirrors cannot become canonical governance without a committed Bifrost document",
|
|
]),
|
|
boundary("patron", "Bifrost", [
|
|
"record patron pressure",
|
|
"surface reward influence",
|
|
"emit standing receipts",
|
|
], [
|
|
"does not charge cards",
|
|
"does not execute external payout rails",
|
|
]),
|
|
boundary("project", "Bifrost plus project maintainers", [
|
|
"link repositories",
|
|
"publish work boards",
|
|
"surface maintainer authority",
|
|
], [
|
|
"does not seize repo Persona cognition or project-local ownership",
|
|
]),
|
|
boundary("account", "Bifrost consumes Heimdall claims", [
|
|
"display membership status",
|
|
"consume linked-account grants",
|
|
"record Bifrost audit trails",
|
|
], [
|
|
"does not mint identity grants",
|
|
"does not own OAuth provider truth",
|
|
]),
|
|
boundary("bridge", "Bifrost", [
|
|
"prepare governed public crossings",
|
|
"execute approved handoffs",
|
|
"record receipts",
|
|
"post Persona-flaired Reddit organizing threads",
|
|
], [
|
|
"does not treat local protocol JSON as work authority",
|
|
"does not touch secrets in this advertisement",
|
|
]),
|
|
],
|
|
styleCapabilities: [
|
|
style("density", ["compact TUI grids", "operator scan tables", "member-facing board/list/detail lowerings"]),
|
|
style("visualEncoding", ["status badges", "priority swatches", "role/tier labels", "receipt/audit timelines"]),
|
|
style("interaction", ["tabs", "filters", "segmented modes", "claim/review/vote command affordances", "read-only witness inspection"]),
|
|
style("loweringTargets", ["Eve native", "browser/Razor transitional lowering", "compact TUI", "future room/overlay projections"]),
|
|
style("tone", ["quiet operational product", "governance/labor clarity", "no crypto/DAO styling", "no separate marketing dashboard truth"]),
|
|
],
|
|
demotions: [
|
|
"Razor Pages are browser lowerings, not the canonical presentation owner.",
|
|
"HTTP health/readiness JSON is a probe, not service truth.",
|
|
"Discord messages are mirrors and input surfaces until Bifrost commits typed state.",
|
|
"Reddit threads are organizing surfaces until Bifrost commits typed votes, priority signals, comments, or receipts.",
|
|
"Local dispatch JSON is evidence for receipts, not command authority.",
|
|
"This advertisement is read-only discovery metadata and does not migrate runtime state.",
|
|
],
|
|
});
|
|
}
|
|
|
|
async function collectStats(options) {
|
|
const generatedAt = options["generated-at"] ?? new Date().toISOString();
|
|
const health = await fetchProbe("http://127.0.0.1:5080/healthz");
|
|
const ready = await fetchProbe("http://127.0.0.1:5080/readyz");
|
|
const transport = runJsonTool(["tools/agent-transport.mjs", "list", "--json"]);
|
|
const governance = runJsonTool(["tools/governance-threads.mjs", "list", "--json"]);
|
|
const docker = dockerContainers();
|
|
const witnesses = [
|
|
witnessStat(".bifrost/provider-advertisement.cc"),
|
|
witnessStat(".bifrost/governance-threads.cc"),
|
|
witnessStat(".bifrost/agent-transport.cc"),
|
|
witnessStat(".bifrost/discord-webhook-cache.json"),
|
|
];
|
|
const topics = Array.isArray(governance.value) ? governance.value : [];
|
|
const requests = Array.isArray(transport.value) ? transport.value : [];
|
|
const topicCounts = countBy(topics, "status");
|
|
const requestCounts = countBy(requests, "status");
|
|
const recentWindowHours = 24;
|
|
const dockerHealthy = docker.items.filter((item) => /healthy/i.test(item.status)).length;
|
|
const dockerRunning = docker.items.filter((item) => /^Up\b/i.test(item.status)).length;
|
|
|
|
return {
|
|
generatedAt,
|
|
health,
|
|
ready,
|
|
docker,
|
|
governance: {
|
|
ok: governance.ok,
|
|
error: governance.error,
|
|
count: topics.length,
|
|
statusCounts: topicCounts,
|
|
recentCount: countRecent(topics, recentWindowHours),
|
|
channelCounts: countBy(topics, "sourceKind"),
|
|
recentChannelCounts: countRecentBy(topics, "sourceKind", recentWindowHours),
|
|
latestUpdatedAt: latestTimestamp(topics.map((item) => item.updatedAt)),
|
|
},
|
|
transport: {
|
|
ok: transport.ok,
|
|
error: transport.error,
|
|
count: requests.length,
|
|
statusCounts: requestCounts,
|
|
recentCount: countRecent(requests, recentWindowHours),
|
|
channelCounts: countBy(requests, "sourceKind"),
|
|
recentChannelCounts: countRecentBy(requests, "sourceKind", recentWindowHours),
|
|
latestUpdatedAt: latestTimestamp(requests.map((item) => item.updatedAt)),
|
|
},
|
|
witnesses,
|
|
bridge: {
|
|
discordPost: true,
|
|
discordDm: true,
|
|
redditPost: Boolean(process.env.BIFROST_REDDIT_CLIENT_ID && process.env.BIFROST_REDDIT_REFRESH_TOKEN),
|
|
githubDraftPr: true,
|
|
githubPrComment: true,
|
|
credentialSource: process.env.BIFROST_DISCORD_BOT_TOKEN ? "BIFROST_DISCORD_BOT_TOKEN" : process.env.DISCORD_BOT_TOKEN ? "DISCORD_BOT_TOKEN-fallback" : "missing",
|
|
redditCredentialSource: process.env.BIFROST_REDDIT_CLIENT_ID && process.env.BIFROST_REDDIT_REFRESH_TOKEN ? "BIFROST_REDDIT_CLIENT_ID+BIFROST_REDDIT_REFRESH_TOKEN" : "missing",
|
|
},
|
|
summary: {
|
|
status: health.ok && ready.ok ? "ready" : "degraded",
|
|
health: health.value ?? health.error,
|
|
ready: ready.value ?? ready.error,
|
|
dockerRunning,
|
|
dockerHealthy,
|
|
governanceTopics: topics.length,
|
|
transportRequests: requests.length,
|
|
recentGovernanceTopics: countRecent(topics, recentWindowHours),
|
|
recentTransportRequests: countRecent(requests, recentWindowHours),
|
|
openTopics: topicCounts.open ?? 0,
|
|
queuedRequests: requestCounts.queued ?? 0,
|
|
recentWindowHours,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function fetchProbe(url) {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 2500);
|
|
const response = await fetch(url, { signal: controller.signal });
|
|
clearTimeout(timeout);
|
|
const text = (await response.text()).trim();
|
|
return {
|
|
ok: response.ok,
|
|
statusCode: response.status,
|
|
value: text,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
function runJsonTool(args) {
|
|
const result = spawnSync(process.execPath, args, {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
windowsHide: true,
|
|
});
|
|
if (result.status !== 0) {
|
|
return {
|
|
ok: false,
|
|
error: (result.stderr || result.stdout || `exit ${result.status}`).trim(),
|
|
value: null,
|
|
};
|
|
}
|
|
try {
|
|
return {
|
|
ok: true,
|
|
value: JSON.parse(result.stdout),
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
value: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
function dockerContainers() {
|
|
const result = spawnSync("docker", ["ps", "--filter", "name=bifrost", "--format", "{{json .}}"], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
windowsHide: true,
|
|
});
|
|
if (result.status !== 0) {
|
|
return {
|
|
ok: false,
|
|
error: (result.stderr || result.stdout || `exit ${result.status}`).trim(),
|
|
items: [],
|
|
};
|
|
}
|
|
const items = result.stdout
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
try {
|
|
const parsed = JSON.parse(line);
|
|
return {
|
|
id: parsed.ID,
|
|
name: parsed.Names,
|
|
image: parsed.Image,
|
|
status: parsed.Status,
|
|
ports: parsed.Ports,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(Boolean);
|
|
return { ok: true, items };
|
|
}
|
|
|
|
function witnessStat(relativePath) {
|
|
const fullPath = resolve(repoRoot, relativePath);
|
|
if (!existsSync(fullPath)) {
|
|
return {
|
|
path: relativePath,
|
|
exists: false,
|
|
updatedAt: null,
|
|
};
|
|
}
|
|
const stat = statSync(fullPath);
|
|
return {
|
|
path: relativePath,
|
|
exists: true,
|
|
updatedAt: stat.mtime.toISOString(),
|
|
};
|
|
}
|
|
|
|
function buildOperatorSurface(stats) {
|
|
const statusTone = stats.summary.status === "ready" ? "ok" : "warn";
|
|
const children = [
|
|
panelNode("service", "Service", [
|
|
metricNode("status", "Status", stats.summary.status, statusTone),
|
|
metricNode("daemon", "Daemon", `health ${stats.summary.health} / ready ${stats.summary.ready}`, stats.health.ok && stats.ready.ok ? "ok" : "warn"),
|
|
metricNode("containers", "Containers", `${stats.summary.dockerRunning} up / ${stats.summary.dockerHealthy} healthy`, stats.summary.dockerHealthy > 0 ? "ok" : "warn"),
|
|
metricNode("stores", "Stores", witnessHealthLine(stats.witnesses), witnessHealthTone(stats.witnesses)),
|
|
metricNode("bridge", "Bridge", `Discord post ${yesNo(stats.bridge.discordPost)} / DM ${yesNo(stats.bridge.discordDm)} / Reddit ${yesNo(stats.bridge.redditPost)}`, stats.bridge.redditPost ? "ok" : "warn"),
|
|
]),
|
|
panelNode("activity", "Activity", [
|
|
metricNode("topics", "Topics", `${stats.summary.governanceTopics} total / ${stats.summary.openTopics} open / ${stats.summary.recentGovernanceTopics} in ${stats.summary.recentWindowHours}h`, stats.governance.ok ? "ok" : "warn"),
|
|
metricNode("requests", "Requests", `${stats.summary.transportRequests} total / ${stats.summary.queuedRequests} queued / ${stats.summary.recentTransportRequests} in ${stats.summary.recentWindowHours}h`, stats.transport.ok ? "ok" : "warn"),
|
|
listNode("status", "Status", [
|
|
`topics: ${compactCounts(stats.governance.statusCounts)}`,
|
|
`requests: ${compactCounts(stats.transport.statusCounts)}`,
|
|
]),
|
|
listNode("channels", "Dispatch Channels", compactChannelLines(stats.transport.channelCounts, stats.transport.recentChannelCounts, stats.summary.recentWindowHours)),
|
|
]),
|
|
];
|
|
|
|
return {
|
|
providerId: "bifrost",
|
|
title: "Bifrost Operator Dashboard",
|
|
version: Date.parse(stats.generatedAt) || Date.now(),
|
|
updatedAt: stats.generatedAt,
|
|
stats,
|
|
surface: {
|
|
schema: "gamecult.eve.surface.v1",
|
|
id: "bifrost-operator-dashboard",
|
|
title: "Bifrost Operator Dashboard",
|
|
root: {
|
|
id: "bifrost-root",
|
|
kind: "dashboard",
|
|
props: {
|
|
title: "Bifrost",
|
|
subtitle: "Governance, labor, and public crossing bridge",
|
|
status: stats.summary.status,
|
|
generatedAt: stats.generatedAt,
|
|
},
|
|
children,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildInterfaceBinding(surface, stats) {
|
|
return {
|
|
bindingId: "bifrost",
|
|
providerId: "bifrost",
|
|
title: surface.title,
|
|
kind: "operator-dashboard",
|
|
updatedAt: surface.updatedAt,
|
|
provider: {
|
|
id: "bifrost",
|
|
title: "Bifrost",
|
|
description: "Bifrost-owned operator stats and bridge health surface.",
|
|
version: String(surface.version),
|
|
endpoint: `${locatedService}/eve/tui`,
|
|
cultMeshAddress: `${locatedService}/eve/tui`,
|
|
canonicalService,
|
|
locatedService,
|
|
plannedLocatedService,
|
|
endpoints: [
|
|
endpoint("operator-tui", `${locatedService}/eve/tui`, "gamecult.eve.surface.v1", ["tui", "nightwing-tui"]),
|
|
endpoint("operator-gui", `${locatedService}/eve/gui`, "gamecult.eve.surface.v1", ["gui", "browser", "eve-native"]),
|
|
],
|
|
routes: [
|
|
route("cultcache-witness", ".bifrost/provider-advertisement.cc", "local-cultcache", true),
|
|
route("websocket-bridge", "ws://192.168.1.66:8797/eve/deck", "compatibility-eve-deck", true),
|
|
],
|
|
capabilities: ["operator-stats", "bridge-health", "governance-counts", "agent-transport-counts", "motion-surface", "motion-commands"],
|
|
usesCultMesh: true,
|
|
status: stats.summary.status,
|
|
transport: "CultMesh Eve interface binding.",
|
|
},
|
|
surface: surface.surface,
|
|
rendererHints: {
|
|
preferredLowerings: ["nightwing-tui", "eve-native", "browser"],
|
|
tileId: "bifrost",
|
|
minWidth: 80,
|
|
minHeight: 18,
|
|
preferredWidth: 120,
|
|
preferredHeight: 32,
|
|
density: "operator",
|
|
},
|
|
};
|
|
}
|
|
|
|
function panelNode(id, title, children) {
|
|
return {
|
|
id: `panel-${id}`,
|
|
kind: "panel",
|
|
props: { title },
|
|
children,
|
|
};
|
|
}
|
|
|
|
function metricNode(id, label, value, tone) {
|
|
return {
|
|
id: `metric-${id}`,
|
|
kind: "metric",
|
|
props: { label, value, tone },
|
|
};
|
|
}
|
|
|
|
function listNode(id, title, items) {
|
|
return {
|
|
id: `list-${id}`,
|
|
kind: "list",
|
|
props: { title },
|
|
children: items.length > 0
|
|
? items.map((item, index) => ({
|
|
id: `list-${id}-${index}`,
|
|
kind: "text",
|
|
props: { text: item },
|
|
}))
|
|
: [{ id: `list-${id}-empty`, kind: "text", props: { text: "none" } }],
|
|
};
|
|
}
|
|
|
|
function compactCounts(value) {
|
|
const entries = Object.entries(value).sort((left, right) => left[0].localeCompare(right[0]));
|
|
return entries.length > 0 ? entries.map(([key, count]) => `${key} ${count}`).join(" / ") : "none";
|
|
}
|
|
|
|
function compactChannelLines(totalCounts, recentCounts, windowHours) {
|
|
const keys = new Set([...Object.keys(totalCounts), ...Object.keys(recentCounts)]);
|
|
const lines = [...keys]
|
|
.sort((left, right) => left.localeCompare(right))
|
|
.map((key) => `${key}: ${totalCounts[key] || 0} total / ${recentCounts[key] || 0} in ${windowHours}h`);
|
|
return lines.length > 0 ? lines : ["none"];
|
|
}
|
|
|
|
function countBy(items, field) {
|
|
const counts = {};
|
|
for (const item of items) {
|
|
const key = String(item?.[field] || "unknown");
|
|
counts[key] = (counts[key] || 0) + 1;
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
function countRecent(items, hours) {
|
|
const cutoff = Date.now() - hours * 60 * 60 * 1000;
|
|
return items.filter((item) => {
|
|
const timestamp = Date.parse(item?.updatedAt || item?.createdAt || "");
|
|
return Number.isFinite(timestamp) && timestamp >= cutoff;
|
|
}).length;
|
|
}
|
|
|
|
function countRecentBy(items, field, hours) {
|
|
const cutoff = Date.now() - hours * 60 * 60 * 1000;
|
|
const counts = {};
|
|
for (const item of items) {
|
|
const timestamp = Date.parse(item?.updatedAt || item?.createdAt || "");
|
|
if (!Number.isFinite(timestamp) || timestamp < cutoff) {
|
|
continue;
|
|
}
|
|
const key = String(item?.[field] || "unknown");
|
|
counts[key] = (counts[key] || 0) + 1;
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
function latestTimestamp(values) {
|
|
return values
|
|
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
.sort()
|
|
.at(-1) || null;
|
|
}
|
|
|
|
function witnessHealthLine(witnesses) {
|
|
const missing = witnesses.filter((item) => !item.exists).length;
|
|
if (missing > 0) {
|
|
return `${witnesses.length - missing}/${witnesses.length} present`;
|
|
}
|
|
const latest = latestTimestamp(witnesses.map((item) => item.updatedAt));
|
|
return `all present / latest ${latest ? shortTime(latest) : "unknown"}`;
|
|
}
|
|
|
|
function witnessHealthTone(witnesses) {
|
|
return witnesses.every((item) => item.exists) ? "ok" : "warn";
|
|
}
|
|
|
|
function shortTime(value) {
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return "unknown";
|
|
}
|
|
return date.toISOString().slice(5, 16).replace("T", " ");
|
|
}
|
|
|
|
function yesNo(value) {
|
|
return value ? "yes" : "no";
|
|
}
|
|
|
|
function namespace(id, purpose) {
|
|
return { id, purpose };
|
|
}
|
|
|
|
function schema(id, type, purpose) {
|
|
return { id, type, purpose };
|
|
}
|
|
|
|
function witness(path, schemaIds, status, purpose) {
|
|
return { path, schemaIds, status, purpose };
|
|
}
|
|
|
|
function surface(id, name, namespace, schemaId, witnessPath, resourceBase, capabilities) {
|
|
return {
|
|
id,
|
|
name,
|
|
namespace,
|
|
schemaId,
|
|
witnessPath,
|
|
capabilities,
|
|
cultMeshAddress: `${locatedService}/${resourceBase}/tui`,
|
|
graphicalAddress: `${locatedService}/${resourceBase}/gui`,
|
|
endpoints: [
|
|
endpoint("tui", `${locatedService}/${resourceBase}/tui`, schemaId, ["tui"]),
|
|
endpoint("gui", `${locatedService}/${resourceBase}/gui`, schemaId, ["gui"]),
|
|
],
|
|
};
|
|
}
|
|
|
|
function boundary(area, owner, commands, forbiddenAuthority) {
|
|
return { area, owner, commands, forbiddenAuthority };
|
|
}
|
|
|
|
function style(area, capabilities) {
|
|
return { area, capabilities };
|
|
}
|
|
|
|
function endpoint(id, address, schemaId, lowerings) {
|
|
return { id, address, schemaId, lowerings };
|
|
}
|
|
|
|
function route(id, address, transport, demoted) {
|
|
return { id, address, transport, demoted };
|
|
}
|
|
|
|
function parseAdvertisement(input) {
|
|
if (!input || typeof input !== "object") {
|
|
throw new Error("Provider advertisement must be an object.");
|
|
}
|
|
|
|
const advertisement = {
|
|
providerId: requireString(input.providerId, "providerId"),
|
|
serviceName: requireString(input.serviceName, "serviceName"),
|
|
contractPath: requireString(input.contractPath, "contractPath"),
|
|
generatedAt: requireString(input.generatedAt, "generatedAt"),
|
|
rootVerse: optionalString(input.rootVerse, rootVerse),
|
|
canonicalService: optionalString(input.canonicalService, canonicalService),
|
|
locatedService: optionalString(input.locatedService, locatedService),
|
|
plannedLocatedService: typeof input.plannedLocatedService === "string" ? input.plannedLocatedService.trim() : "",
|
|
cultMeshAddress: optionalString(input.cultMeshAddress, locatedService),
|
|
endpoints: Array.isArray(input.endpoints) ? requireObjectArray(input.endpoints, "endpoints") : [],
|
|
routes: Array.isArray(input.routes) ? requireObjectArray(input.routes, "routes") : [],
|
|
authority: requireObject(input.authority, "authority"),
|
|
namespaces: requireObjectArray(input.namespaces, "namespaces"),
|
|
schemas: requireObjectArray(input.schemas, "schemas"),
|
|
witnesses: requireObjectArray(input.witnesses, "witnesses"),
|
|
surfaces: requireObjectArray(input.surfaces, "surfaces"),
|
|
commandBoundaries: requireObjectArray(input.commandBoundaries, "commandBoundaries"),
|
|
styleCapabilities: requireObjectArray(input.styleCapabilities, "styleCapabilities"),
|
|
demotions: requireStringArray(input.demotions, "demotions"),
|
|
};
|
|
|
|
const requiredSurfaces = new Set(["bifrost.work", "bifrost.motion", "bifrost.patron", "bifrost.project", "bifrost.account"]);
|
|
const surfaceIds = new Set(advertisement.surfaces.map((surface) => surface.id));
|
|
for (const id of requiredSurfaces) {
|
|
if (!surfaceIds.has(id)) {
|
|
throw new Error(`Provider advertisement must name ${id}.`);
|
|
}
|
|
}
|
|
|
|
return advertisement;
|
|
}
|
|
|
|
function parseObjectDocument(label) {
|
|
return (input) => {
|
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
throw new Error(`${label} must be an object.`);
|
|
}
|
|
return input;
|
|
};
|
|
}
|
|
|
|
function parseArgs(args) {
|
|
const options = {};
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const token = args[index];
|
|
if (!token.startsWith("--")) {
|
|
throw new Error(`Unexpected argument "${token}".`);
|
|
}
|
|
|
|
const key = token.slice(2);
|
|
const next = args[index + 1];
|
|
if (!next || next.startsWith("--")) {
|
|
options[key] = "true";
|
|
continue;
|
|
}
|
|
|
|
options[key] = next;
|
|
index += 1;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function requireString(value, field) {
|
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
throw new Error(`Provider advertisement field "${field}" must be a non-empty string.`);
|
|
}
|
|
|
|
return value.trim();
|
|
}
|
|
|
|
function optionalString(value, fallback) {
|
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
return value.trim();
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function requireObject(value, field) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new Error(`Provider advertisement field "${field}" must be an object.`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function requireObjectArray(value, field) {
|
|
if (!Array.isArray(value) || value.some((item) => !item || typeof item !== "object" || Array.isArray(item))) {
|
|
throw new Error(`Provider advertisement field "${field}" must be an array of objects.`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function requireStringArray(value, field) {
|
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
throw new Error(`Provider advertisement field "${field}" must be an array of non-empty strings.`);
|
|
}
|
|
|
|
return value.map((item) => item.trim());
|
|
}
|
|
|
|
function resolveOptionPath(path) {
|
|
return resolve(process.cwd(), path);
|
|
}
|
|
|
|
function printJson(value) {
|
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function printHelp() {
|
|
process.stdout.write(`Bifrost Eve provider advertisement
|
|
|
|
Commands:
|
|
export Write the Bifrost gamecult.eve.provider_advertisement.v1 document to a CultCache .cc witness
|
|
print Print the advertisement as protocol-debug JSON without writing state
|
|
schema Print document type metadata
|
|
|
|
Options:
|
|
--out <path> Override export path; defaults to .bifrost/provider-advertisement.cc
|
|
--generated-at <iso> Pin generatedAt for deterministic fixture checks
|
|
|
|
Examples:
|
|
node tools/provider-advertisement.mjs print
|
|
node tools/provider-advertisement.mjs export --out .bifrost/provider-advertisement.cc
|
|
`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exitCode = 1;
|
|
});
|