Files
world/scripts/gen-analyst-commands.mjs
hanzo-dev e98ed9b093 feat(world/analyst): app-command registry, model dropdown, transport seam
PART 1 — one introspectable command registry + one dispatcher (decomplected):
- src/services/app-commands.ts is the single source of truth: AppHost capability
  port + 16 typed commands (name, JSON-schema params, human description, run()) +
  commandManifest() + the ONE dispatch(): gate (sign-in) → validate → execute →
  visible action log. Full control now covers panels (show/hide/move/resize),
  map (layers, 2D/3D, fly-to, region, time range), variant, theme, search,
  reset layout, custom feeds, and org switch — each driving existing App APIs
  through the port (no reaching into internals).
- analyst-actions.ts reduced to a type shim (AnalystHost=AppHost). AnalystChat
  dispatches through the registry and renders per-command ✓/✗ entries.
- Go tool contract DERIVES from the registry: the client sends its manifest with
  every request; handlers_analyst.go builds the action-contract prompt from it
  (renderCommandContract) and passes actions through generically, gated by the
  manifest's type set — the backend no longer duplicates the schema. Embedded
  mirror data/analyst_commands.json (generated by scripts/gen-analyst-commands.mjs,
  drift-guarded by TestAnalystCommandManifest) covers callers that omit a manifest.

Model dropdown + backend plane (CTO directive):
- Chat header carries a monochrome model select (Zen family first, then upstream
  /v1/models + agents); choice persists and rides every request. New same-origin
  /v1/world/models proxy; model passes through to inference (chatMessagesModel).
  "agent:<ref>" ids route to the agents run plane (invoke an agent in-chat).

P0 — errors always visible: the chat never shows a blank. An empty/degraded/errored
backend reply (e.g. prod's aud=hanzo-world 401) now surfaces the reason inline.

PART 2 — transport seam: src/services/analyst-transport.ts isolates the wire; HTTP
is the real impl, registerTransport() is the ZAP seam. Verdict: real browser ZAP
(@zap-proto/web) exists and is production-wired in hanzo/platform, but world has no
ZAP dep and no Go WS-ZAP server, and ZAP has no streaming primitive today — so HTTP
stays default, ZAP wiring documented in-file (no fantasy transport).

Gates: tsc + vite build + go build + go test ./internal/world/ all pass; Playwright
(e2e/analyst-command.spec.ts) proves signed-out sign-in prompt and a signed-in
command round-trip that visibly moves a panel + logs the action (screens/).
2026-07-09 22:00:13 -07:00

41 lines
1.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Generate the backend's static command manifest mirror from the ONE source of
* truth — src/services/app-commands.ts (the client registry).
*
* The client always sends its live manifest with each analyst request, so the
* runtime contract has zero drift by construction. This generated file is the
* FALLBACK the Go backend embeds for callers that don't send one, and the Go
* test (TestAnalystCommandManifest) verifies it stays structurally sound and
* complete. Re-run after changing the registry:
*
* node scripts/gen-analyst-commands.mjs
*
* app-commands.ts imports nothing, so esbuild transpiles it standalone.
*/
import { build } from 'esbuild';
import { writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const SRC = new URL('../src/services/app-commands.ts', import.meta.url).pathname;
const OUT = new URL('../internal/world/data/analyst_commands.json', import.meta.url).pathname;
const res = await build({
entryPoints: [SRC],
bundle: true,
format: 'esm',
platform: 'neutral',
write: false,
});
const tmp = join(mkdtempSync(join(tmpdir(), 'zap-cmds-')), 'app-commands.mjs');
writeFileSync(tmp, res.outputFiles[0].text);
const mod = await import(pathToFileURL(tmp).href);
const manifest = mod.commandManifest();
writeFileSync(OUT, JSON.stringify(manifest, null, 2) + '\n');
console.log(`wrote ${manifest.length} commands → ${OUT}`);