Make Huginn emit Eve DSL only

This commit is contained in:
Meta Vens 2026-06-01 22:15:39 +01:00
commit a079386a3e
26 changed files with 177 additions and 8027 deletions

View file

@ -38,47 +38,34 @@ CultCache files may use the `.cc` extension. The bytes are still the canonical
`cultcache.store.v1` MessagePack snapshot; the extension is the human handle, not
a second format.
Run the local inspector during development:
Huginn is the runtime steward for `.cc` and Persona-state inspection. VoidBot may
carry compatibility MCP calls while the swarm is still wired that way, but
inspection ownership lives here: schema catalogs, persisted envelopes, decoded
payload previews, read-only state traversal, and the Eve DSL that lets any
runtime render those facts without becoming the state owner.
```sh
npm run dev:inspector
```
Huginn does not ship an Electron app, React renderer, Vite dashboard, or local
presentation runtime. That was the wrong owner. Huginn speaks CultMesh and Eve:
it inspects bytes, emits Eve DSL, and lets the consuming runtime lower that DSL
into browser, native, overlay, or other local UI.
Build the Vite inspector bundle:
`inspectCultCacheBytes(...)` reads `.cc`, `.msgpack`, or `.mpack` bytes and
returns the snapshot header, schema catalog, records, and decoded MessagePack
payload previews without registering application schemas.
```sh
npm run build:inspector
```
`buildHuginnEveDsl(...)` lowers that inspection into Eve DSL for provider id
`cultcache.huginn.inspector`. The `.cc` bytes remain authority; the Eve surface
compiled from the DSL is a read-only operator witness:
Build the desktop package:
- source file path, format, and byte count;
- schema catalog entries, versions, ids, and member counts;
- persisted records, schema names, timestamps, payload byte counts, and decode
status.
```sh
npm run dist:inspector
```
Release builds must carry a new semantic version before any deployable artifact
is produced. While the package is pre-1.0, breaking public behavior increments
the minor version, compatible fixes increment the patch version, and the
generated Huginn executable must use that package version in its filename.
Huginn is read-only. Drop a `.cc`, `.msgpack`, or `.mpack` file onto the window to
inspect the snapshot header, schema catalog, records, decoded MessagePack
payload previews, and an Norn graph cloud of the file's structured payload data
without registering application schemas.
Huginn's background field uses the graph node AABB as its single world-space
authority. The WebGPU particle pass keeps a million particle slots available,
intersects the current screen bounds with the artwork bounds, and assigns only
visible quadtree cells to those slots. Each visible cell is seeded with the
Aetheria Stardust `RandomFirst` hash and xorshift follow-up sequence, then each
particle pair trades opacity with sine/cosine phase while tracing a short
segment through the packed flow texture. The packed map uses `RG` for flow, `B`
for structure/curvature/detail pressure, and `A` for emission/field strength;
particle color comes from the source Huginn albedo at the starting UV so the
flow carries brushstrokes instead of repainting them at the final sample.
Zooming raises the live quadtree level; coarse cells fade down while finer cells
seeded from the same spatial lattice fade in around them, so detail density
rises without changing the field's identity.
The next runtime cut is a Huginn CultMesh provider that publishes the same Eve
DSL directly so Eve clients can browse `.cc` files as dashboards from inside
their own runtime instead of asking VoidBot to impersonate a state inspector over
MCP.
## Example

3
dist/huginn-eve-dsl.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
import type { CultCacheInspection } from "./cult-cache-inspector";
export declare function buildHuginnEveDsl(inspection: CultCacheInspection): string;
//# sourceMappingURL=huginn-eve-dsl.d.ts.map

1
dist/huginn-eve-dsl.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"huginn-eve-dsl.d.ts","sourceRoot":"","sources":["../src/huginn-eve-dsl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAA0C,MAAM,wBAAwB,CAAC;AAE1G,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,mBAAmB,GAAG,MAAM,CA8BzE"}

55
dist/huginn-eve-dsl.js vendored Normal file
View file

@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildHuginnEveDsl = buildHuginnEveDsl;
function buildHuginnEveDsl(inspection) {
return [
"# Huginn emits Eve DSL. Renderers compile and lower it.",
`surface cultcache.huginn.inspector ${quoteDsl("Huginn .cc Inspector")}`,
"version 1",
"",
"collection huginn.catalog.entries",
...inspection.catalog.map((entry) => `item huginn.catalog.entries ${quoteDsl(renderCatalogEntry(entry))}`),
"",
"collection huginn.records.entries",
...inspection.records.map((record) => `item huginn.records.entries ${quoteDsl(renderRecord(record))}`),
"",
"card source \"Source Witness\"",
`text ${quoteDsl(`file: ${inspection.filePath}`)}`,
`text ${quoteDsl(`format: ${inspection.format}`)}`,
`text ${quoteDsl(`bytes: ${inspection.fileSizeBytes}`)}`,
"text \".cc bytes are authority; Huginn only emits Eve DSL.\"",
"end",
"",
"card catalog \"Schema Catalog\"",
`text ${quoteDsl(`schemas: ${inspection.catalog.length}`)}`,
"list \"Catalog Entries\" bind huginn.catalog.entries",
"end",
"",
"card records \"Persisted Records\"",
`text ${quoteDsl(`records: ${inspection.records.length}`)}`,
"list \"Record Entries\" bind huginn.records.entries",
"end",
"",
].join("\n");
}
function renderCatalogEntry(entry) {
return [
entry.schemaName,
entry.schemaVersion,
entry.schemaId,
`${entry.members.length} members`,
].join(" | ");
}
function renderRecord(record) {
const status = record.payloadDecodeError ? `decode error: ${record.payloadDecodeError}` : "decoded";
return [
record.key,
record.schemaName,
record.storedAt,
`${record.payloadBytes} bytes`,
status,
].join(" | ");
}
function quoteDsl(value) {
return JSON.stringify(value);
}

1
dist/index.d.ts vendored
View file

@ -1,4 +1,5 @@
export { CultCache, CultCacheBuilder } from "./cult-cache";
export { buildHuginnEveDsl } from "./huginn-eve-dsl";
export { inspectCultCacheBytes } from "./cult-cache-inspector";
export { defineDocumentRegistry, defineDocumentType } from "./document";
export { SingleFileMessagePackBackingStore } from "./single-file-messagepack-backing-store";

2
dist/index.d.ts.map vendored
View file

@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,iCAAiC,EAAE,MAAM,yCAAyC,CAAC;AAC5F,YAAY,EACV,8BAA8B,EAC9B,iBAAiB,EACjB,yBAAyB,EACzB,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,gCAAgC,EAChC,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,EAC5B,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,0BAA0B,EAC1B,cAAc,GACf,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,GAChB,MAAM,wBAAwB,CAAC"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,iCAAiC,EAAE,MAAM,yCAAyC,CAAC;AAC5F,YAAY,EACV,8BAA8B,EAC9B,iBAAiB,EACjB,yBAAyB,EACzB,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,gCAAgC,EAChC,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,EAC5B,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,0BAA0B,EAC1B,cAAc,GACf,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,GAChB,MAAM,wBAAwB,CAAC"}

4
dist/index.js vendored
View file

@ -1,9 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SingleFileMessagePackBackingStore = exports.defineDocumentType = exports.defineDocumentRegistry = exports.inspectCultCacheBytes = exports.CultCacheBuilder = exports.CultCache = void 0;
exports.SingleFileMessagePackBackingStore = exports.defineDocumentType = exports.defineDocumentRegistry = exports.inspectCultCacheBytes = exports.buildHuginnEveDsl = exports.CultCacheBuilder = exports.CultCache = void 0;
var cult_cache_1 = require("./cult-cache");
Object.defineProperty(exports, "CultCache", { enumerable: true, get: function () { return cult_cache_1.CultCache; } });
Object.defineProperty(exports, "CultCacheBuilder", { enumerable: true, get: function () { return cult_cache_1.CultCacheBuilder; } });
var huginn_eve_dsl_1 = require("./huginn-eve-dsl");
Object.defineProperty(exports, "buildHuginnEveDsl", { enumerable: true, get: function () { return huginn_eve_dsl_1.buildHuginnEveDsl; } });
var cult_cache_inspector_1 = require("./cult-cache-inspector");
Object.defineProperty(exports, "inspectCultCacheBytes", { enumerable: true, get: function () { return cult_cache_inspector_1.inspectCultCacheBytes; } });
var document_1 = require("./document");

View file

@ -1,28 +0,0 @@
{
"appId": "com.gamecult.cultcache.inspector",
"productName": "Huginn",
"executableName": "Huginn",
"asar": false,
"icon": "inspector/public/hugin.ico",
"extraMetadata": {
"main": "electron/main.cjs"
},
"files": [
"electron/main.cjs",
"dist-inspector/**/*"
],
"directories": {
"output": "release-inspector"
},
"win": {
"signAndEditExecutable": false,
"target": ["portable"],
"artifactName": "Huginn-${version}.${ext}"
},
"mac": {
"target": ["dir"]
},
"linux": {
"target": ["dir"]
}
}

View file

@ -1,72 +0,0 @@
const { app, BrowserWindow } = require("electron");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const devServer = process.env.CULTCACHE_INSPECTOR_DEV_SERVER;
app.setName("Huginn");
const logPath = path.join(app.getPath("userData"), "huginn-renderer.log");
function writeLog(message) {
const line = `[${new Date().toISOString()}] ${message}${os.EOL}`;
try {
fs.mkdirSync(path.dirname(logPath), { recursive: true });
fs.appendFileSync(logPath, line);
} catch {
// Logging must never be the reason the inspector refuses to open.
}
console.log(line.trimEnd());
}
function createWindow() {
const window = new BrowserWindow({
width: 1180,
height: 780,
minWidth: 820,
minHeight: 560,
title: "Huginn",
icon: path.join(__dirname, "..", "dist-inspector", "hugin-64.png"),
backgroundColor: "#05090d",
autoHideMenuBar: true,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
window.webContents.on("console-message", (_event, level, message, line, sourceId) => {
writeLog(`renderer console[${level}] ${sourceId}:${line} ${message}`);
});
window.webContents.on("did-fail-load", (_event, code, description, validatedUrl) => {
writeLog(`renderer failed load ${code} ${description} ${validatedUrl}`);
});
window.webContents.on("render-process-gone", (_event, details) => {
writeLog(`renderer process gone ${details.reason} exitCode=${details.exitCode}`);
});
window.webContents.on("did-finish-load", () => {
writeLog(`renderer loaded ${window.webContents.getURL()}`);
});
if (devServer) {
void window.loadURL(devServer);
} else {
void window.loadFile(path.join(__dirname, "..", "dist-inspector", "index.html"));
}
}
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});

View file

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Huginn</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because it is too large Load diff

View file

@ -1,856 +0,0 @@
import { NornViewer, type NornGraphEdge, type NornGraphNode, type NornGraphsState, type ViewerSelection } from "@gamecult/norn-viewer";
import { createRoot } from "react-dom/client";
import { Component, useMemo, useState, type DragEvent, type ReactNode } from "react";
import { inspectCultCacheBytes, type CultCacheInspection, type InspectedCatalogEntry, type InspectedRecord } from "../../src/cult-cache-inspector";
import { HuginnFieldCanvas } from "./HuginnFieldCanvas";
import "./styles.css";
type Selection = {
record: number;
};
type NodeArticle = NonNullable<Parameters<typeof NornViewer>[0]["nodeArticle"]>;
type RawSelection =
| { kind: "record"; index: number }
| { kind: "value"; recordIndex: number; path: string; value: unknown };
type GraphProjection = {
state: NornGraphsState;
recordNodeIds: string[];
nodeSelections: Map<string, RawSelection>;
edgeSelections: Map<string, RawSelection>;
collapsedPayloadValues: number;
};
const MAX_PAYLOAD_REGION_NODES = 160;
const MAX_RECORD_REGION_NODES = 32;
const MAX_PAYLOAD_REGION_DEPTH = 3;
const MAX_PAYLOAD_REGION_CHILDREN = 10;
const MAX_NESTED_PAYLOAD_REGION_CHILDREN = 6;
const assetPath = (name: string) => `${import.meta.env.BASE_URL}${name}`;
const HUGINN_ART = {
surface: assetPath("huginn-surface.png"),
field: assetPath("huginn-fieldpack.png"),
};
function HuginnField() {
return (
<HuginnFieldCanvas
imageUrl={HUGINN_ART.surface}
fieldUrl={HUGINN_ART.field}
/>
);
}
const app = document.querySelector<HTMLDivElement>("#app");
if (!app) {
throw new Error("Missing inspector app root.");
}
class RenderCrashBoundary extends Component<{ children: ReactNode }, { error: string | null }> {
state = { error: null };
static getDerivedStateFromError(error: unknown): { error: string } {
return { error: error instanceof Error ? error.message : String(error) };
}
render(): ReactNode {
if (this.state.error) {
return (
<main className="workspace">
<section className="sidebar">
<div className="brand">
<img src={assetPath("hugin-64.png")} alt="" width="64" height="64" />
<div>
<h1>Huginn</h1>
<p>CultCache state inspection for <code>.cc</code> files.</p>
</div>
</div>
</section>
<section className="content">
<div className="error fatal-error">
<strong>Renderer error</strong>
<span>{this.state.error}</span>
</div>
</section>
</main>
);
}
return this.props.children;
}
}
createRoot(app).render(
<RenderCrashBoundary>
<HuginnApp />
</RenderCrashBoundary>,
);
function HuginnApp() {
const [inspection, setInspection] = useState<CultCacheInspection | undefined>();
const [selection, setSelection] = useState<Selection>({ record: 0 });
const [graphSelection, setGraphSelection] = useState<ViewerSelection | null>(null);
const [errorMessage, setErrorMessage] = useState("");
const [dragging, setDragging] = useState(false);
const graphProjection = useMemo(
() => inspection ? buildGraphProjection(inspection) : emptyGraphProjection(),
[inspection],
);
async function inspectFile(file: File): Promise<void> {
try {
const bytes = new Uint8Array(await file.arrayBuffer());
const nextInspection = inspectCultCacheBytes(file.name, bytes, file.size);
setInspection(nextInspection);
setSelection({ record: 0 });
setGraphSelection({
kind: "node",
graphKey: "dataflow",
nodeId: nextInspection.records[0]
? recordNodeId(nextInspection.records[0], 0)
: "store",
});
setErrorMessage("");
} catch (error) {
setInspection(undefined);
setErrorMessage(error instanceof Error ? error.message : String(error));
}
}
function handleDrop(event: DragEvent<HTMLElement>): void {
event.preventDefault();
setDragging(false);
const file = event.dataTransfer.files.item(0);
if (file) {
void inspectFile(file);
}
}
return (
<main
className={`workspace ${dragging ? "is-dragging" : ""}`}
onDragOver={(event) => {
event.preventDefault();
setDragging(true);
}}
onDragLeave={(event) => {
if (event.target === event.currentTarget) {
setDragging(false);
}
}}
onDrop={handleDrop}
>
{inspection
? (
<InspectionView
errorMessage={errorMessage}
inspection={inspection}
graphProjection={graphProjection}
graphSelection={graphSelection}
setGraphSelection={setGraphSelection}
selection={selection}
setSelection={setSelection}
onInspectFile={inspectFile}
/>
)
: (
<EmptyWorkspace
errorMessage={errorMessage}
onInspectFile={inspectFile}
/>
)}
</main>
);
}
function EmptyWorkspace({
errorMessage,
onInspectFile,
}: {
errorMessage: string;
onInspectFile: (file: File) => Promise<void>;
}) {
return (
<section className="empty-workspace">
<HuginnField />
<section className="floating-panel brand-panel is-primary">
<BrandBlock />
<FilePicker onInspectFile={onInspectFile} />
{errorMessage ? <div className="error">{errorMessage}</div> : null}
<EmptySummary />
</section>
<NoFile />
</section>
);
}
function BrandBlock() {
return (
<div className="brand">
<img src={assetPath("hugin-64.png")} alt="" width="64" height="64" />
<div>
<h1>Huginn</h1>
<p>CultCache state inspection for <code>.cc</code> files.</p>
</div>
</div>
);
}
function FilePicker({ onInspectFile }: { onInspectFile: (file: File) => Promise<void> }) {
return (
<label className="dropzone">
<input
type="file"
accept=".cc,.msgpack,.mpack,application/octet-stream"
onChange={(event) => {
const file = event.currentTarget.files?.item(0);
if (file) {
void onInspectFile(file);
}
}}
/>
<span className="drop-title">Drop CultCache file</span>
<span className="drop-copy">or choose one from disk</span>
</label>
);
}
function Summary({ inspection }: { inspection: CultCacheInspection }) {
return (
<div className="summary">
<Metric label="Format" value={inspection.format} />
<Metric label="Records" value={inspection.records.length.toString()} />
<Metric label="Schemas" value={inspection.catalog.length.toString()} />
<Metric label="Bytes" value={inspection.fileSizeBytes.toString()} />
</div>
);
}
function EmptySummary() {
return (
<div className="summary">
<Metric label="Format" value=".cc" />
<Metric label="Mode" value="Read-only" />
<Metric label="Payload" value="MessagePack" />
<Metric label="Runtime" value="Vite" />
</div>
);
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="metric">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function NoFile() {
return (
<div className="hero">
<img src={HUGINN_ART.surface} alt="" />
<h2>No state loaded</h2>
<p>Drag a CultCache <code>.cc</code> store into the window to inspect the snapshot header, schema catalog, records, and decoded MessagePack payloads.</p>
</div>
);
}
function InspectionView({
errorMessage,
inspection,
graphProjection,
graphSelection,
setGraphSelection,
selection,
setSelection,
onInspectFile,
}: {
errorMessage: string;
inspection: CultCacheInspection;
graphProjection: GraphProjection;
graphSelection: ViewerSelection | null;
setGraphSelection: (selection: ViewerSelection | null) => void;
selection: Selection;
setSelection: (selection: Selection) => void;
onInspectFile: (file: File) => Promise<void>;
}) {
const selectRaw = (next: Extract<RawSelection, { kind: "record" }>) => {
setSelection({ record: next.index });
setGraphSelection({
kind: "node",
graphKey: "dataflow",
nodeId: graphProjection.recordNodeIds[next.index],
});
};
const selectGraph = (next: ViewerSelection | null) => {
setGraphSelection(next);
if (!next) {
return;
}
const rawSelection = next.kind === "node"
? graphProjection.nodeSelections.get(next.nodeId)
: graphProjection.edgeSelections.get(next.edgeId);
if (!rawSelection) {
return;
}
if (rawSelection.kind === "record") {
setSelection({ record: rawSelection.index });
return;
}
if (rawSelection.kind === "value") {
setSelection({ record: rawSelection.recordIndex });
return;
}
};
const nodeArticle: NodeArticle = {
className: "huginn-expanded-node",
ariaLabel: (node) => `${node.title} CultCache detail`,
content: (node) => {
const rawSelection = graphProjection.nodeSelections.get(node.id);
const record = rawSelection?.kind === "record"
? inspection.records[rawSelection.index]
: rawSelection?.kind === "value"
? inspection.records[rawSelection.recordIndex]
: undefined;
const expandedValue = rawSelection?.kind === "value"
? {
path: rawSelection.path,
value: rawSelection.value,
}
: undefined;
const catalogEntry = record
? inspection.catalog.find((entry) => entry.schemaId === record.schemaId)
: undefined;
return (
<ExpandedNodePanel
catalogEntry={catalogEntry}
expandedValue={expandedValue}
inspection={inspection}
record={record}
/>
);
},
};
return (
<div className="inspector-stage">
<section className="graph-panel">
<NornViewer
className="huginn-graph-shell"
state={graphProjection.state}
initialGraph="dataflow"
selection={graphSelection}
onSelectionChange={selectGraph}
title="CultCache Structure"
viewportBackdrop={<HuginnField />}
viewportBackground="#03070a"
layoutMode={{ architecture: "force", dataflow: "force" }}
motion={{
strength: 1.24,
damping: 0.88,
flow: 1.2,
orbit: 0.84,
lift: 0.9,
pulse: 1,
emitNodeEnvelopes: true,
}}
overlayPanels
showSidebar={false}
nodeArticle={nodeArticle}
graphLabels={{ architecture: "File", dataflow: "Payload Cloud" }}
style={{ minHeight: "100vh" }}
/>
{graphProjection.collapsedPayloadValues > 0 ? (
<div className="graph-warning">
Payload cloud collapsed {graphProjection.collapsedPayloadValues} leaf/deep value(s) into article detail. The graph shows records and inspectable regions, not every scalar slot.
</div>
) : null}
</section>
<div className="overlay-layer">
<section className="floating-panel brand-panel">
<BrandBlock />
<FilePicker onInspectFile={onInspectFile} />
{errorMessage ? <div className="error">{errorMessage}</div> : null}
<Summary inspection={inspection} />
</section>
<section className="floating-panel data-panel records-panel">
<PanelHeader title="Records" count={inspection.records.length.toString()} />
<div className="list">
{inspection.records.length
? inspection.records.map((entry, index) => (
<RecordButton
key={`${entry.schemaId}:${entry.key}:${index}`}
record={entry}
index={index}
selected={index === selection.record}
onSelect={() => selectRaw({ kind: "record", index })}
/>
))
: <div className="empty">No records</div>}
</div>
</section>
</div>
</div>
);
}
function PanelHeader({ title, count }: { title: string; count: string }) {
return (
<header>
<h2>{title}</h2>
<span>{count}</span>
</header>
);
}
function ExpandedNodePanel({
catalogEntry,
expandedValue,
inspection,
record,
}: {
catalogEntry: InspectedCatalogEntry | undefined;
expandedValue: { path: string; value: unknown } | undefined;
inspection: CultCacheInspection;
record: InspectedRecord | undefined;
}) {
const title = expandedValue?.path ?? record?.key ?? catalogEntry?.schemaName ?? inspection.filePath;
const payloadValue = expandedValue?.value ?? record?.payloadPreview;
return (
<article className="expanded-content">
<div className="expanded-kicker">{expandedValue ? "Payload Value" : "CultCache Node"}</div>
<header className="expanded-header">
<h1>{title}</h1>
<div className="expanded-chips">
<span>{inspection.format}</span>
{record ? <span>{record.schemaName}</span> : <span>{inspection.records.length} records</span>}
{expandedValue
? <span>{valueStatus(expandedValue.value)}</span>
: record ? <span>{record.payloadBytes} bytes</span> : <span>{inspection.catalog.length} schemas</span>}
</div>
</header>
<section className="expanded-summary">
{expandedValue && record ? (
<p>{expandedValue.path} inside {record.key}.</p>
) : record ? (
<>
<p>{record.schemaName} record stored at {record.storedAt}.</p>
{record.payloadDecodeError ? <div className="error">{record.payloadDecodeError}</div> : null}
</>
) : (
<p>{inspection.filePath} contains a schema catalog and persisted MessagePack record set.</p>
)}
</section>
{payloadValue !== undefined ? (
<section className="expanded-payload">
<h2>Payload</h2>
<PayloadCode value={payloadValue} />
</section>
) : null}
<section className="expanded-article">
<div>
<h2>Record Metadata</h2>
{record ? <RecordFacts record={record} /> : <div className="empty">Select a record node to inspect payload data.</div>}
</div>
<div>
<h2>Schema Entry</h2>
{catalogEntry ? <CatalogDetail entry={catalogEntry} /> : <div className="empty">Select a schema node to inspect catalog data.</div>}
</div>
</section>
<footer className="expanded-footer">
Record {record ? inspection.records.indexOf(record) + 1 : "-"} of {inspection.records.length}
</footer>
</article>
);
}
function RecordButton({
record,
index,
selected,
onSelect,
}: {
record: InspectedRecord;
index: number;
selected: boolean;
onSelect: () => void;
}) {
return (
<button className="row" type="button" data-record={index} aria-selected={selected} onClick={onSelect}>
<strong>{record.key}</strong>
<span>{record.schemaName} - {record.payloadBytes} bytes</span>
</button>
);
}
function RecordFacts({ record }: { record: InspectedRecord }) {
return (
<Facts rows={[
["Key", record.key],
["Schema", record.schemaName],
["Schema ID", record.schemaId],
["Stored", record.storedAt],
["Payload", `${record.payloadBytes} bytes`],
]} />
);
}
function PayloadCode({ value }: { value: unknown }) {
return (
<pre className="payload-code">{JSON.stringify(value, null, 2)}</pre>
);
}
function CatalogDetail({ entry }: { entry: InspectedCatalogEntry }) {
return (
<>
<Facts rows={[
["Schema", entry.schemaName],
["Version", entry.schemaVersion],
["Schema ID", entry.schemaId],
["Hash", entry.contentHash],
["Compatible", entry.compatibleSchemaIds.join(", ")],
]} />
<pre>{JSON.stringify({
members: entry.members,
canonicalSchemaJson: parseJson(entry.canonicalSchemaJson),
}, null, 2)}</pre>
</>
);
}
function Facts({ rows }: { rows: Array<[string, string]> }) {
return (
<dl>
{rows.map(([name, value]) => (
<div className="fact-row" key={name}>
<dt>{name}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
);
}
function buildGraphProjection(inspection: CultCacheInspection): GraphProjection {
const dataflowNodes: NornGraphNode[] = [];
const dataflowEdges: NornGraphEdge[] = [];
const recordNodeIds: string[] = [];
const nodeSelections = new Map<string, RawSelection>();
const edgeSelections = new Map<string, RawSelection>();
let collapsedPayloadValues = 0;
const architectureNodes: NornGraphNode[] = [
{
id: "store",
title: ".cc Store",
purpose: `${inspection.format} file with ${inspection.records.length} record(s) and ${inspection.catalog.length} schema catalog entr${inspection.catalog.length === 1 ? "y" : "ies"}.`,
mechanism: inspection.filePath,
status: "source",
},
{
id: "catalog",
title: "Schema Catalog",
purpose: "Maps persisted schema ids to schema names, versions, compatible ids, and slot metadata.",
status: `${inspection.catalog.length} schemas`,
},
{
id: "records",
title: "Records",
purpose: "Persisted document entries carrying key, schema id, timestamp, and raw MessagePack payload bytes.",
status: `${inspection.records.length} records`,
},
];
const architectureEdges: NornGraphEdge[] = [
{
id: "store-catalog",
source_id: "store",
target_id: "catalog",
kind: "contains",
label: "schema catalog",
},
{
id: "store-records",
source_id: "store",
target_id: "records",
kind: "contains",
label: "record set",
},
];
dataflowNodes.push({
id: "store",
title: ".cc Store",
purpose: `Payload regions for ${inspection.format} snapshot.`,
mechanism: inspection.filePath,
status: `${inspection.fileSizeBytes} bytes`,
});
inspection.records.forEach((record, recordIndex) => {
const currentRecordNodeId = recordNodeId(record, recordIndex);
const recordSelection: RawSelection = { kind: "record", index: recordIndex };
const perRecordBudget = {
remainingRegionNodes: Math.min(
MAX_RECORD_REGION_NODES,
Math.floor(MAX_PAYLOAD_REGION_NODES / Math.max(1, inspection.records.length)),
),
collapsedPayloadValues: 0,
};
recordNodeIds[recordIndex] = currentRecordNodeId;
nodeSelections.set(currentRecordNodeId, recordSelection);
dataflowNodes.push({
id: currentRecordNodeId,
title: record.key,
purpose: valuePurpose(record.payloadPreview),
mechanism: record.schemaName,
status: `${record.payloadBytes} bytes`,
});
const recordEdgeId = edgeId("store", currentRecordNodeId, "contains");
edgeSelections.set(recordEdgeId, recordSelection);
dataflowEdges.push({
id: recordEdgeId,
source_id: "store",
target_id: currentRecordNodeId,
kind: "contains",
label: "record",
});
appendPayloadRegions({
nodes: dataflowNodes,
edges: dataflowEdges,
nodeSelections,
edgeSelections,
recordIndex,
parentId: currentRecordNodeId,
value: record.payloadPreview,
path: "payload",
depth: 0,
budget: perRecordBudget,
});
collapsedPayloadValues += perRecordBudget.collapsedPayloadValues;
});
return {
state: {
architecture: { nodes: architectureNodes, edges: architectureEdges },
dataflow: { nodes: dataflowNodes, edges: dataflowEdges },
links: [],
},
recordNodeIds,
nodeSelections,
edgeSelections,
collapsedPayloadValues,
};
}
function appendPayloadRegions({
nodes,
edges,
nodeSelections,
edgeSelections,
recordIndex,
parentId,
value,
path,
depth,
budget,
}: {
nodes: NornGraphNode[];
edges: NornGraphEdge[];
nodeSelections: Map<string, RawSelection>;
edgeSelections: Map<string, RawSelection>;
recordIndex: number;
parentId: string;
value: unknown;
path: string;
depth: number;
budget: { remainingRegionNodes: number; collapsedPayloadValues: number };
}): void {
if (depth >= MAX_PAYLOAD_REGION_DEPTH) {
budget.collapsedPayloadValues += countPayloadDescendants(value);
return;
}
const entries = payloadRegionEntries(value, path);
if (!entries.length) {
return;
}
let emittedChildren = 0;
const childLimit = depth === 0 ? MAX_PAYLOAD_REGION_CHILDREN : MAX_NESTED_PAYLOAD_REGION_CHILDREN;
for (const entry of entries) {
if (!isGraphworthyRegion(entry.value, depth)) {
budget.collapsedPayloadValues += 1 + countPayloadDescendants(entry.value);
continue;
}
if (budget.remainingRegionNodes <= 0 || emittedChildren >= childLimit) {
budget.collapsedPayloadValues += 1 + countPayloadDescendants(entry.value);
continue;
}
budget.remainingRegionNodes -= 1;
emittedChildren += 1;
const childId = nodeId("region", `${parentId}:${entry.path}`);
const rawSelection: RawSelection = { kind: "value", recordIndex, path: entry.path, value: entry.value };
nodeSelections.set(childId, rawSelection);
nodes.push(regionNode(childId, entry.label, entry.value, entry.path));
const edge = valueEdge(parentId, childId, entry.kind, entry.label);
edgeSelections.set(edge.id!, rawSelection);
edges.push(edge);
appendPayloadRegions({
nodes,
edges,
nodeSelections,
edgeSelections,
recordIndex,
parentId: childId,
value: entry.value,
path: entry.path,
depth: depth + 1,
budget,
});
}
}
function payloadRegionEntries(
value: unknown,
path: string,
): Array<{ label: string; value: unknown; path: string; kind: "field" | "slot" }> {
if (Array.isArray(value)) {
return value.map((item, index) => ({
label: `[${index}]`,
value: item,
path: `${path}[${index}]`,
kind: "slot",
}));
}
if (isPlainObject(value)) {
return Object.entries(value).map(([key, item]) => ({
label: key,
value: item,
path: `${path}.${key}`,
kind: "field",
}));
}
return [];
}
function isGraphworthyRegion(value: unknown, depth: number): boolean {
if (!Array.isArray(value) && !isPlainObject(value)) {
return false;
}
const directChildren = payloadRegionEntries(value, "payload").length;
if (directChildren >= (depth === 0 ? 2 : 4)) {
return true;
}
return countPayloadDescendants(value, 64) >= 10;
}
function countPayloadDescendants(value: unknown, limit = 20_000): number {
let count = 0;
const stack: unknown[] = [value];
while (stack.length && count < limit) {
const current = stack.pop();
const children = Array.isArray(current)
? current
: isPlainObject(current)
? Object.values(current)
: [];
for (const child of children) {
count += 1;
if (Array.isArray(child) || isPlainObject(child)) {
stack.push(child);
}
if (count >= limit) {
break;
}
}
}
return count;
}
function regionNode(id: string, title: string, value: unknown, path: string): NornGraphNode {
return {
id,
title,
purpose: valuePurpose(value),
mechanism: path,
status: `${valueStatus(value)} / ${countPayloadDescendants(value)} values`,
};
}
function recordNodeId(record: InspectedRecord, index: number): string {
return nodeId("record", `${record.schemaId}:${record.key}:${index}`);
}
function valueEdge(sourceId: string, targetId: string, kind: string, label: string): NornGraphEdge {
return {
id: edgeId(sourceId, targetId, kind),
source_id: sourceId,
target_id: targetId,
kind,
label,
};
}
function valuePurpose(value: unknown): string {
if (Array.isArray(value)) {
return `Array with ${value.length} item(s).`;
}
if (isPlainObject(value)) {
return `Object with ${Object.keys(value).length} field(s).`;
}
if (value === null) {
return "Null payload value.";
}
return JSON.stringify(value) ?? String(value);
}
function valueStatus(value: unknown): string {
if (Array.isArray(value)) {
return "array";
}
if (isPlainObject(value)) {
return "object";
}
return typeof value;
}
function emptyGraphProjection(): GraphProjection {
return {
state: {
architecture: { nodes: [], edges: [] },
dataflow: { nodes: [], edges: [] },
links: [],
},
recordNodeIds: [],
nodeSelections: new Map(),
edgeSelections: new Map(),
collapsedPayloadValues: 0,
};
}
function parseJson(value: string): unknown {
try {
return JSON.parse(value);
} catch {
return value;
}
}
function nodeId(prefix: string, value: string): string {
return `${prefix}:${value.replace(/[^a-zA-Z0-9:_-]/g, "_")}`;
}
function edgeId(sourceId: string, targetId: string, kind: string): string {
return `${sourceId}->${targetId}:${kind}`;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View file

@ -1,726 +0,0 @@
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #dce9ee;
background: #05090d;
--bg: #05090d;
--panel: #0b131b;
--panel-strong: #101c27;
--ink: #dce9ee;
--muted: #8296a3;
--line: #1e3444;
--line-bright: #1e7fa4;
--cyan: #33e6f3;
--cyan-soft: rgba(51, 230, 243, 0.16);
--cyan-glow: rgba(51, 230, 243, 0.36);
--brass: #d49a3d;
--danger: #ff897d;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 760px;
min-height: 100vh;
overflow: hidden;
}
button,
input {
font: inherit;
}
code {
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
font-size: 0.92em;
}
.workspace {
position: relative;
display: block;
min-height: 100vh;
background:
radial-gradient(circle at 78% 18%, rgba(39, 202, 239, 0.12), transparent 30%),
linear-gradient(135deg, #05090d 0%, #07111a 48%, #030507 100%);
}
.sidebar {
border-right: 1px solid var(--line);
background: linear-gradient(180deg, rgba(14, 27, 38, 0.98), rgba(5, 9, 13, 0.98));
padding: 22px;
box-shadow: 8px 0 36px rgba(0, 0, 0, 0.32);
}
.brand {
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
gap: 14px;
align-items: center;
}
.brand img {
border-radius: 8px;
image-rendering: pixelated;
filter: drop-shadow(0 0 14px var(--cyan-glow));
}
.brand h1 {
margin: 0;
color: #f0fbff;
font-size: 30px;
line-height: 1.05;
letter-spacing: 0;
}
.brand p {
margin: 7px 0 18px;
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
.dropzone {
display: grid;
gap: 6px;
place-items: center;
min-height: 132px;
border: 1px dashed #276982;
border-radius: 8px;
background: linear-gradient(180deg, rgba(13, 38, 50, 0.72), rgba(6, 15, 22, 0.88));
color: var(--cyan);
cursor: pointer;
text-align: center;
box-shadow: inset 0 0 18px rgba(51, 230, 243, 0.06);
}
.is-dragging .dropzone {
border-color: var(--cyan);
background: rgba(51, 230, 243, 0.12);
box-shadow: 0 0 22px var(--cyan-glow), inset 0 0 22px rgba(51, 230, 243, 0.1);
}
.dropzone input {
position: absolute;
inline-size: 1px;
block-size: 1px;
opacity: 0;
pointer-events: none;
}
.drop-title {
font-weight: 800;
}
.drop-copy {
color: var(--muted);
font-size: 13px;
}
.summary {
display: grid;
gap: 10px;
margin-top: 16px;
}
.metric {
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(12, 23, 32, 0.92);
padding: 11px;
}
.metric span {
display: block;
color: var(--muted);
font-size: 12px;
}
.metric strong {
display: block;
margin-top: 4px;
overflow-wrap: anywhere;
}
.content {
padding: 18px;
overflow: auto;
}
.empty-workspace,
.inspector-stage {
position: fixed;
inset: 0;
overflow: hidden;
background: #03070a;
}
.empty-workspace {
display: grid;
place-items: center;
padding: clamp(18px, 4vw, 56px);
}
.empty-workspace .huginn-field-canvas {
opacity: 0.46;
}
.overlay-layer {
position: fixed;
inset: 0;
z-index: 12;
pointer-events: none;
}
.graph-panel {
position: fixed;
inset: 0;
min-width: 0;
border: 0;
border-radius: 0;
overflow: hidden;
background: #03070a;
}
.graph-panel > section {
width: 100vw !important;
height: 100vh !important;
min-height: 100vh !important;
}
.huginn-field-canvas {
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
image-rendering: auto;
opacity: 0.96;
pointer-events: none;
}
.norn-overlay-panel {
background: rgba(4, 11, 16, 0.72) !important;
border-color: rgba(51, 230, 243, 0.22) !important;
box-shadow: 0 0 28px rgba(51, 230, 243, 0.08), 0 18px 48px rgba(0, 0, 0, 0.28) !important;
}
.huginn-graph-shell {
width: 100vw;
height: 100vh;
}
.huginn-graph-shell svg {
filter: drop-shadow(0 0 12px rgba(51, 230, 243, 0.28));
}
.huginn-graph-shell svg path[stroke]:not([stroke="transparent"]) {
opacity: 1;
stroke-width: 2.4px;
}
.huginn-graph-shell svg rect[filter] {
fill-opacity: 0.94;
stroke-opacity: 1;
}
.huginn-graph-shell svg text {
fill-opacity: 1;
paint-order: stroke;
stroke: rgba(1, 6, 10, 0.72);
stroke-width: 2px;
stroke-linejoin: round;
}
.huginn-graph-shell .norn-overlay-panel,
.floating-panel {
opacity: 0.28;
transition:
opacity 160ms ease,
transform 160ms ease,
box-shadow 160ms ease,
background-color 160ms ease;
}
.huginn-graph-shell .norn-overlay-panel:hover,
.huginn-graph-shell .norn-overlay-panel:focus-within,
.floating-panel:hover,
.floating-panel:focus-within,
.floating-panel.is-primary {
opacity: 1;
transform: translateY(-2px);
}
.floating-panel {
position: absolute;
pointer-events: auto;
container-type: inline-size;
border: 1px solid rgba(51, 230, 243, 0.22);
border-radius: 24px;
background: rgba(4, 11, 16, 0.72);
padding: clamp(10px, 1.4vw, 18px);
box-shadow: 0 20px 54px rgba(0, 0, 0, 0.34), 0 0 28px rgba(51, 230, 243, 0.08);
backdrop-filter: blur(16px);
}
.brand-panel {
top: 16px;
left: 16px;
width: min(330px, calc(100vw - 32px));
transform: translateX(-242px);
}
.brand-panel:hover,
.brand-panel:focus-within,
.brand-panel.is-primary {
transform: translateX(0);
}
.records-panel {
left: 16px;
bottom: 16px;
width: min(360px, calc(50vw - 24px));
max-height: min(44vh, 420px);
transform: translateY(calc(100% - 48px));
}
.records-panel:hover,
.records-panel:focus-within {
transform: translate(0, 0);
}
.graph-warning {
position: fixed;
left: 50%;
bottom: 18px;
z-index: 14;
max-width: min(720px, calc(100vw - 36px));
transform: translateX(-50%);
border-top: 1px solid rgba(212, 154, 61, 0.3);
border: 1px solid rgba(212, 154, 61, 0.3);
border-radius: 18px;
background: rgba(72, 45, 12, 0.34);
color: #ffd38a;
padding: 10px 12px;
font-size: 12px;
line-height: 1.35;
}
.hero {
position: relative;
z-index: 2;
display: grid;
align-content: center;
justify-items: center;
min-height: calc(100vh - 36px);
max-width: 680px;
text-align: center;
}
.hero img {
width: 168px;
height: 168px;
margin-bottom: 18px;
image-rendering: auto;
filter: drop-shadow(0 0 28px var(--cyan-glow));
}
.hero h2 {
margin: 0;
color: #f0fbff;
font-size: 42px;
line-height: 1;
text-shadow: 0 0 18px rgba(3, 7, 10, 0.92), 0 0 34px rgba(51, 230, 243, 0.28);
}
.hero p {
margin: 14px 0 0;
color: var(--muted);
line-height: 1.55;
text-shadow: 0 0 14px rgba(3, 7, 10, 0.96);
}
.panel {
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(8, 16, 23, 0.94);
overflow: hidden;
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.24);
}
.panel header,
.floating-panel header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
border-bottom: 1px solid var(--line);
padding: 11px 12px;
background: rgba(16, 28, 39, 0.92);
}
.panel h2,
.floating-panel h2 {
margin: 0;
color: #f0fbff;
font-size: 15px;
}
.panel header span,
.floating-panel header span {
overflow: hidden;
color: var(--muted);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.list {
display: grid;
gap: 8px;
max-height: inherit;
overflow: auto;
padding: 10px;
}
.row {
width: 100%;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel-strong);
color: inherit;
padding: 10px;
text-align: left;
cursor: pointer;
}
.row[aria-selected="true"] {
border-color: var(--cyan);
box-shadow: inset 3px 0 0 var(--cyan), 0 0 18px rgba(51, 230, 243, 0.12);
}
.row strong,
.row span {
display: block;
overflow-wrap: anywhere;
}
.row span {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
}
dl {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
gap: 8px 12px;
}
dt {
color: var(--muted);
font-size: 12px;
}
dd {
margin: 0;
overflow-wrap: anywhere;
}
pre {
max-height: min(440px, 42vh);
overflow: auto;
border-radius: 6px;
border: 1px solid #142b39;
background: #03080c;
color: #dffbff;
padding: 12px;
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
font-size: 12px;
line-height: 1.45;
}
.error {
border: 1px solid rgba(255, 137, 125, 0.42);
border-radius: 6px;
background: rgba(94, 27, 24, 0.28);
color: var(--danger);
padding: 10px;
overflow-wrap: anywhere;
}
.fatal-error {
display: grid;
gap: 8px;
max-width: 720px;
margin: 32px;
padding: 16px;
}
.empty {
padding: 18px;
color: var(--muted);
}
.huginn-expanded-node {
scrollbar-color: rgba(51, 230, 243, 0.58) rgba(3, 7, 10, 0.32);
transition:
border-radius 180ms ease,
box-shadow 180ms ease,
padding 180ms ease;
}
.expanded-content {
display: grid;
gap: clamp(10px, 1.4vw, 18px);
min-height: 100%;
padding: clamp(14px, 2.2vw, 30px);
color: #e7fbff;
}
.expanded-kicker {
color: var(--cyan);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.expanded-header {
display: grid;
gap: 10px;
}
.expanded-header h1 {
margin: 0;
overflow-wrap: anywhere;
color: #f8fdff;
font-size: clamp(1.45rem, 3.4vw, 3rem);
line-height: 1;
}
.expanded-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.expanded-chips span,
.expanded-footer {
border: 1px solid rgba(51, 230, 243, 0.22);
border-radius: 999px;
background: rgba(5, 18, 26, 0.68);
color: #aeeef5;
padding: 6px 9px;
font-size: 12px;
font-weight: 700;
}
.expanded-summary {
color: rgba(231, 251, 255, 0.78);
line-height: 1.55;
}
.expanded-payload {
display: grid;
gap: 8px;
min-width: 0;
min-height: 0;
}
.expanded-payload h2 {
margin: 0;
color: #f8fdff;
font-size: 18px;
}
.expanded-article {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
min-height: 0;
}
.expanded-article h2 {
margin: 0 0 8px;
color: #f8fdff;
font-size: 18px;
}
.expanded-footer {
width: fit-content;
}
.huginn-expanded-node[data-node-stage="summary"] {
overflow: hidden;
}
.huginn-expanded-node[data-node-stage="summary"] .expanded-content {
display: grid;
align-items: center;
justify-items: center;
gap: 6px;
padding: 10px 14px;
text-align: center;
}
.huginn-expanded-node[data-node-stage="summary"] .expanded-kicker,
.huginn-expanded-node[data-node-stage="summary"] .expanded-summary,
.huginn-expanded-node[data-node-stage="summary"] .expanded-article,
.huginn-expanded-node[data-node-stage="summary"] .expanded-footer {
display: none;
}
.huginn-expanded-node[data-node-stage="summary"] .expanded-header {
display: grid;
justify-items: center;
min-width: 0;
}
.huginn-expanded-node[data-node-stage="summary"] .expanded-header h1 {
max-width: 100%;
font-size: 1.02rem;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.huginn-expanded-node[data-node-stage="summary"] .expanded-chips {
justify-content: center;
}
.huginn-expanded-node[data-node-stage="summary"] .expanded-payload {
display: none;
}
.huginn-expanded-node[data-node-stage="preview"] {
overflow: hidden;
}
.huginn-expanded-node[data-node-stage="preview"] .expanded-content {
align-content: center;
justify-items: center;
gap: 10px;
padding: 18px 22px;
text-align: center;
}
.huginn-expanded-node[data-node-stage="preview"] .expanded-header {
justify-items: center;
max-width: min(640px, 100%);
}
.huginn-expanded-node[data-node-stage="preview"] .expanded-header h1 {
max-width: 100%;
font-size: clamp(1.05rem, 2.1vw, 1.7rem);
text-align: center;
}
.huginn-expanded-node[data-node-stage="preview"] .expanded-chips {
justify-content: center;
}
.huginn-expanded-node[data-node-stage="preview"] .expanded-summary,
.huginn-expanded-node[data-node-stage="preview"] .expanded-article,
.huginn-expanded-node[data-node-stage="preview"] .expanded-footer {
display: none;
}
.huginn-expanded-node[data-node-stage="preview"] .expanded-payload {
width: min(680px, 100%);
justify-items: center;
}
.huginn-expanded-node[data-node-stage="preview"] .expanded-payload h2 {
color: var(--cyan);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.huginn-expanded-node[data-node-stage="preview"] .payload-code {
width: 100%;
max-height: min(280px, 44vh);
margin: 0;
text-align: left;
}
.huginn-expanded-node[data-node-stage="preview"] dl {
display: none;
}
.huginn-expanded-node[data-node-stage="article"] {
overflow: auto;
}
@container (max-width: 260px) {
.brand p,
.drop-copy,
.metric span,
.row span,
.floating-panel header span {
display: none;
}
.brand {
grid-template-columns: 42px minmax(0, 1fr);
}
.brand img {
width: 42px;
height: 42px;
}
.brand h1 {
font-size: 22px;
}
.dropzone {
min-height: 54px;
}
}
@container (max-width: 180px) {
.summary,
.dropzone,
.list {
display: none;
}
.floating-panel header {
border-bottom: 0;
padding: 6px 8px;
}
}
@media (max-width: 900px) {
body {
min-width: 320px;
}
.brand-panel {
width: min(300px, calc(100vw - 24px));
transform: translateX(calc(-100% + 58px));
}
.records-panel {
left: 12px;
right: 12px;
width: auto;
max-height: 32vh;
}
.records-panel {
bottom: 12px;
}
.expanded-article {
grid-template-columns: 1fr;
}
}

4843
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,10 +9,6 @@
],
"scripts": {
"build": "tsc -p tsconfig.json",
"build:inspector": "tsc -p tsconfig.inspector.json && vite build --config vite.inspector.config.ts",
"dev:inspector": "concurrently -k \"vite --host 127.0.0.1 --config vite.inspector.config.ts\" \"wait-on http://127.0.0.1:5173 && cross-env CULTCACHE_INSPECTOR_DEV_SERVER=http://127.0.0.1:5173 electron electron/main.cjs\"",
"dist:inspector": "npm run build:inspector && electron-builder --config electron-builder.json",
"pack:inspector": "npm run build:inspector && electron-builder --config electron-builder.json --dir",
"typecheck": "tsc -p tsconfig.json --pretty false",
"test": "npm run build && tsc -p tsconfig.test.json && node --test dist-test/test/cult-cache.test.js"
},
@ -31,19 +27,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"@gamecult/norn-viewer": "file:../Norn/web/norn-viewer",
"@types/node": "^24.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"electron": "^42.1.0",
"electron-builder": "^26.8.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"typescript": "^5.9.0",
"vite": "^8.0.13",
"wait-on": "^9.0.10"
"typescript": "^5.9.0"
}
}

57
src/huginn-eve-dsl.ts Normal file
View file

@ -0,0 +1,57 @@
import type { CultCacheInspection, InspectedCatalogEntry, InspectedRecord } from "./cult-cache-inspector";
export function buildHuginnEveDsl(inspection: CultCacheInspection): string {
return [
"# Huginn emits Eve DSL. Renderers compile and lower it.",
`surface cultcache.huginn.inspector ${quoteDsl("Huginn .cc Inspector")}`,
"version 1",
"",
"collection huginn.catalog.entries",
...inspection.catalog.map((entry) => `item huginn.catalog.entries ${quoteDsl(renderCatalogEntry(entry))}`),
"",
"collection huginn.records.entries",
...inspection.records.map((record) => `item huginn.records.entries ${quoteDsl(renderRecord(record))}`),
"",
"card source \"Source Witness\"",
`text ${quoteDsl(`file: ${inspection.filePath}`)}`,
`text ${quoteDsl(`format: ${inspection.format}`)}`,
`text ${quoteDsl(`bytes: ${inspection.fileSizeBytes}`)}`,
"text \".cc bytes are authority; Huginn only emits Eve DSL.\"",
"end",
"",
"card catalog \"Schema Catalog\"",
`text ${quoteDsl(`schemas: ${inspection.catalog.length}`)}`,
"list \"Catalog Entries\" bind huginn.catalog.entries",
"end",
"",
"card records \"Persisted Records\"",
`text ${quoteDsl(`records: ${inspection.records.length}`)}`,
"list \"Record Entries\" bind huginn.records.entries",
"end",
"",
].join("\n");
}
function renderCatalogEntry(entry: InspectedCatalogEntry): string {
return [
entry.schemaName,
entry.schemaVersion,
entry.schemaId,
`${entry.members.length} members`,
].join(" | ");
}
function renderRecord(record: InspectedRecord): string {
const status = record.payloadDecodeError ? `decode error: ${record.payloadDecodeError}` : "decoded";
return [
record.key,
record.schemaName,
record.storedAt,
`${record.payloadBytes} bytes`,
status,
].join(" | ");
}
function quoteDsl(value: string): string {
return JSON.stringify(value);
}

View file

@ -1,4 +1,5 @@
export { CultCache, CultCacheBuilder } from "./cult-cache";
export { buildHuginnEveDsl } from "./huginn-eve-dsl";
export { inspectCultCacheBytes } from "./cult-cache-inspector";
export { defineDocumentRegistry, defineDocumentType } from "./document";
export { SingleFileMessagePackBackingStore } from "./single-file-messagepack-backing-store";

View file

@ -11,6 +11,7 @@ import { z } from "zod";
import { CultCache } from "../src/cult-cache";
import { inspectCultCacheBytes } from "../src/cult-cache-inspector";
import { buildHuginnEveDsl } from "../src/huginn-eve-dsl";
import { defineDocumentRegistry, defineDocumentType } from "../src/document";
import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store";
import type { CacheBackingStore, CultCacheEnvelope, CultCacheSchema } from "../src/types";
@ -565,6 +566,20 @@ test("CultCache inspector decodes v1 store catalog and record payloads from byte
]);
});
test("Huginn projects .cc inspection into Eve DSL", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "huginn-eve-dsl-"));
const file = join(tempDir, "cache.cc");
const written = await writeTsInteropStore(file, "eve-surface");
const inspection = inspectCultCacheBytes(file, await readFile(file));
const dsl = buildHuginnEveDsl(inspection);
assert.match(dsl, /^surface cultcache\.huginn\.inspector "Huginn \.cc Inspector"/mu);
assert.match(dsl, /collection huginn\.catalog\.entries/u);
assert.match(dsl, /collection huginn\.records\.entries/u);
assert.match(dsl, new RegExp(written.documentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(dsl, /\.cc bytes are authority; Huginn only emits Eve DSL\./u);
});
interface InteropNote {
schemaVersion: "cultcache.interop_note.v1";
documentId: string;

View file

@ -1,17 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"rootDir": ".",
"types": ["vite/client"]
},
"include": [
"inspector/src/**/*.ts",
"inspector/src/**/*.tsx",
"src/cult-cache-inspector.ts"
]
}

View file

@ -1,24 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL(".", import.meta.url));
export default defineConfig({
root: "inspector",
base: "./",
plugins: [react()],
resolve: {
alias: {
react: path.resolve(root, "node_modules/react"),
"react-dom": path.resolve(root, "node_modules/react-dom"),
"react/jsx-runtime": path.resolve(root, "node_modules/react/jsx-runtime.js"),
},
dedupe: ["react", "react-dom"],
},
build: {
outDir: "../dist-inspector",
emptyOutDir: true
}
});