feat: add capability playground to discover
This commit is contained in:
@@ -3,7 +3,7 @@ import type { DraftDiagnostic, DraftWorkspace } from "../domain/draft-workspace-
|
||||
import type { CapabilityDetail, CapabilitySummary } from "../domain/capability-models.js";
|
||||
import { projectAuthoringGraph, type WorkbenchSelection } from "./authoring-graph.js";
|
||||
import { withDiagnosticKeys } from "./diagnostic-key.js";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
import { CapabilityNodeForm } from "./CapabilityNodeForm.js";
|
||||
import { SelectedCapabilityInspector } from "./SelectedCapabilityInspector.js";
|
||||
import { RouteForm } from "./RouteForm.js";
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type SchemaField,
|
||||
} from "../schema-form/schema-field.js";
|
||||
import { serializeSchemaValues, type FieldSources } from "../schema-form/schema-values.js";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
import { displayGraphInputPath, displayLocalInputPath } from "./input-binding-paths.js";
|
||||
import {
|
||||
capabilityLocalPathSuggestions,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
OutputBinding,
|
||||
StatePath,
|
||||
} from "../domain/draft-workspace-models.js";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
import {
|
||||
capabilityLocalPathSuggestions,
|
||||
inferredStateSchemaPreview,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
|
||||
describe("formatBoundedJson", () => {
|
||||
it("bounds traversal without reading remote fields after the budget", () => {
|
||||
const draft = {
|
||||
first: "x".repeat(1_000),
|
||||
get later() {
|
||||
throw new Error("later field should not be read");
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => formatBoundedJson(draft, 80)).not.toThrow();
|
||||
expect(formatBoundedJson(draft, 80)).toHaveLength(80);
|
||||
expect(formatBoundedJson(draft, 80)).toContain("truncated");
|
||||
});
|
||||
|
||||
it("keeps an exact-fit JSON document complete and valid", () => {
|
||||
const draft = { step: "collect", count: 2 };
|
||||
const completeJson = JSON.stringify(draft, null, 2);
|
||||
|
||||
expect(formatBoundedJson(draft, completeJson.length)).toBe(completeJson);
|
||||
expect(JSON.parse(formatBoundedJson(draft, completeJson.length))).toEqual(draft);
|
||||
});
|
||||
|
||||
it("does not truncate a complete document just below the boundary", () => {
|
||||
const draft = { step: "collect", count: 2 };
|
||||
const completeJson = JSON.stringify(draft, null, 2);
|
||||
|
||||
expect(formatBoundedJson(draft, completeJson.length + 1)).toBe(completeJson);
|
||||
expect(formatBoundedJson(draft, completeJson.length - 1)).toContain("truncated");
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
const MAX_RAW_DRAFT_CHARS = 12_000;
|
||||
const TRUNCATION_MARKER = "... truncated ...";
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
// Traverse until the display budget is exhausted so a large remote object is
|
||||
// never fully materialized just to produce a clipped escape-hatch preview.
|
||||
export const formatBoundedJson = (
|
||||
value: unknown,
|
||||
maxChars = MAX_RAW_DRAFT_CHARS,
|
||||
): string => {
|
||||
const truncationMarker = TRUNCATION_MARKER.slice(0, Math.max(0, maxChars));
|
||||
const contentLimit = Math.max(0, maxChars);
|
||||
let output = "";
|
||||
let truncated = false;
|
||||
const activeObjects = new WeakSet<object>();
|
||||
|
||||
const append = (chunk: string): void => {
|
||||
if (truncated) return;
|
||||
if (output.length + chunk.length > contentLimit) {
|
||||
output += chunk.slice(0, Math.max(0, contentLimit - output.length));
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
output += chunk;
|
||||
};
|
||||
|
||||
const appendJsonString = (text: string): void => {
|
||||
append('"');
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (truncated) return;
|
||||
const code = text.charCodeAt(index);
|
||||
if (code === 0x22) append('\\"');
|
||||
else if (code === 0x5c) append("\\\\");
|
||||
else if (code < 0x20) append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
else if (code >= 0xd800 && code <= 0xdbff) {
|
||||
const nextCode = text.charCodeAt(index + 1);
|
||||
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
||||
append(text.slice(index, index + 2));
|
||||
index++;
|
||||
} else append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
||||
append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
} else append(text.charAt(index));
|
||||
}
|
||||
if (!truncated) append('"');
|
||||
};
|
||||
|
||||
const visit = (current: unknown, depth: number): void => {
|
||||
if (truncated) return;
|
||||
if (current === null || typeof current !== "object") {
|
||||
if (typeof current === "string") appendJsonString(current);
|
||||
else if (typeof current === "number") append(Number.isFinite(current) ? String(current) : "null");
|
||||
else if (typeof current === "boolean") append(current ? "true" : "false");
|
||||
else append("null");
|
||||
return;
|
||||
}
|
||||
if (activeObjects.has(current)) {
|
||||
append('"[Circular]"');
|
||||
return;
|
||||
}
|
||||
activeObjects.add(current);
|
||||
const indent = " ".repeat(depth);
|
||||
const childIndent = " ".repeat(depth + 1);
|
||||
if (Array.isArray(current)) {
|
||||
append("[");
|
||||
let first = true;
|
||||
for (const item of current) {
|
||||
if (truncated) break;
|
||||
append(first ? `\n${childIndent}` : `,\n${childIndent}`);
|
||||
visit(item, depth + 1);
|
||||
first = false;
|
||||
}
|
||||
if (!truncated) append(first ? "]" : `\n${indent}]`);
|
||||
} else {
|
||||
if (!isRecord(current)) {
|
||||
activeObjects.delete(current);
|
||||
return;
|
||||
}
|
||||
const record = current;
|
||||
append("{");
|
||||
let first = true;
|
||||
for (const key in record) {
|
||||
if (!Object.prototype.hasOwnProperty.call(record, key) || truncated) continue;
|
||||
append(first ? `\n${childIndent}` : `,\n${childIndent}`);
|
||||
appendJsonString(key);
|
||||
append(": ");
|
||||
visit(record[key], depth + 1);
|
||||
first = false;
|
||||
}
|
||||
if (!truncated) append(first ? "}" : `\n${indent}}`);
|
||||
}
|
||||
activeObjects.delete(current);
|
||||
};
|
||||
|
||||
visit(value, 0);
|
||||
if (!truncated) return output;
|
||||
const markerStart = Math.max(0, contentLimit - truncationMarker.length);
|
||||
return `${output.slice(0, markerStart)}${truncationMarker}`;
|
||||
};
|
||||
Reference in New Issue
Block a user