Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
id: 01M4QMTZTXKP2R3JPG6TY4GFA1
anchor: function investigationDocument
created: 2026-10-12T08:49:30Z
norm: '1'
---

Emit list items joined by single newlines: blank-line-separated items make a loose list that fails the Lexical round trip (room.validate).
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
id: 01M4QMTZX6RSHR0KHHBEVS6F0P
anchor: function owned
created: 2026-10-12T08:49:30Z
norm: '1'
---

A published investigation is reachable from its child document id too (childId); run/retry/cancel stay parent-only.
76 changes: 76 additions & 0 deletions apps/server/src/experiments/document.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect, test } from "bun:test";
import { limits, parse } from "@chopin/dialect";
import { parseResult } from "@chopin/experiment";
import { performance } from "@chopin/experiment/fixtures";
import { canonical } from "../mcp/create";
import { investigationDocument, investigationSummary } from "./document";

const ID = "6f1c3c2e-8b0a-4c43-9d55-2d6f3c1b9a10";

test("a completed result becomes a research-shaped child that round-trips the dialect", () => {
let document = investigationDocument(ID, "Does caching help startup?", performance);
expect(document.title).toBe("Startup comparison");
expect(canonical(document.source)).toEqual({ source: document.source });
let headings = document.source.split("\n").filter(line => line.startsWith("#"));
expect(headings).toEqual(["# Startup comparison", "## Views", "## Method", "## Limitations"]);
expect(document.source).toContain("Caching reduced median startup time");
let embeds = JSON.stringify(parse(document.source)).match(/"name":"Experiment"/g);
expect(embeds).toHaveLength(1);
expect(document.source).toMatch(
new RegExp(`<Experiment id="[0-9A-Z]{26}" experiment="${ID}" view="comparison" />`),
);
expect(investigationSummary("Does caching help startup?", performance)).toBe(
"Caching reduced median startup time in the captured workload.",
);
});

test("the brief names a report without its own heading, and markup stays inert", () => {
let result = parseResult({
...performance,
report: "Plain finding with `code` and {braces}.\n",
views: [],
evidence: [{ key: "log", title: "Run <log>", format: "plain", text: "```\n{x}\n```" }],
provenance: {
environment: "Laptop {env}",
checks: ["<b>ran</b>", "Compared medians"],
limitations: ["One", "Two"],
},
});
let document = investigationDocument(ID, "Measure *startup*\nwith detail", result);
expect(document.title).toBe("Measure startup");
expect(document.source.startsWith("# Measure startup\n")).toBe(true);
expect(canonical(document.source)).toEqual({ source: document.source });
expect(document.source).not.toContain("<Experiment");
expect(document.source).toContain("````\n```\n{x}\n```\n````");
});

test("evidence is clipped to keep the child inside the source limit", () => {
let result = parseResult({
...performance,
evidence: [..."abcdefgh"].map(key => ({
key,
title: key,
format: "plain",
text: "x".repeat(64 * 1024),
})),
report: `# Big\n\n${"word ".repeat(20_000)}\n`,
});
let document = investigationDocument(ID, "Big", result);
expect(Buffer.byteLength(document.source)).toBeLessThanOrEqual(limits.MAX_SOURCE_BYTES);
expect(document.source).toContain("## Evidence");
});

test("a report cannot embed record-backed components into its child", () => {
for (
let component of [
`<Investigation id="${ID}" />`,
`<Experiment id="01K0N4W3B7P27CBAEC7A8C8WEH" experiment="${ID}" view="comparison" />`,
`<Research id="${ID}" />`,
]
) {
let result = parseResult({ ...performance, report: `# Report\n\n${component}\n` });
expect(() => investigationDocument(ID, "Brief", result)).toThrow(
"A report cannot embed record-backed components.",
);
}
});
123 changes: 123 additions & 0 deletions apps/server/src/experiments/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { limits, parse } from "@chopin/dialect";
import { ExperimentError } from "@chopin/experiment";
import type { Result } from "@chopin/experiment";
import { MAX_TITLE_LENGTH } from "../channels/title";
import { canonical } from "../mcp/create";
import { reportText } from "../research/publication";

const SUMMARY_LENGTH = 240;
// Room for the ids canonicalization adds to each embed.
const SOURCE_BUDGET = limits.MAX_SOURCE_BYTES - 16 * 1024;
// Records own these; a submitted report may not mint their projections.
const PROTECTED = /"name":"(Questionnaire|Question|Decision|Research|Investigation|Experiment)"/;
const LANGUAGES = { plain: "", csv: "", json: "json", diff: "diff" } as const;

/** Markdown inline source as the words a reader sees. */
function plain(value: string): string {
return value
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
.replace(/\\(.)|[*`~]/g, (_match, escaped?: string) => escaped ?? "")
.replace(/\s+/g, " ")
.trim();
}

function clip(value: string, length: number): string {
let points = [...value];
return points.length <= length ? value : `${points.slice(0, length - 1).join("").trimEnd()}…`;
}

/** The report's own top heading leads the child; otherwise the brief names it. */
function split(brief: string, report: string): { title: string; lead: string; body: string } {
let match = /^# (.+)\n+/.exec(report);
let heading = match ? plain(match[1]) : "";
if (match && heading) {
return {
title: clip(heading, MAX_TITLE_LENGTH),
lead: match[0].trim(),
body: report.slice(match[0].length),
};
}
let title = clip(plain(brief.split("\n").find(line => line.trim()) ?? "") || "Investigation", 80);
return { title, lead: `# ${reportText(title)}`, body: report };
}

function fence(text: string, language: string): string {
let longest = Math.max(0, ...[...text.matchAll(/`+/g)].map(run => run[0].length));
let marks = "`".repeat(Math.max(3, longest + 1));
return `${marks}${language}\n${text.replace(/\n$/, "")}\n${marks}`;
}

/** One tight list; blank lines between items would make it loose and fail the round trip. */
function list(items: string[]): string {
return items.map(item => `- ${reportText(item)}`).join("\n");
}

function bytes(value: string): number {
return Buffer.byteLength(value, "utf8");
}

/** The first prose paragraph of a completed report, for the card that links to its child. */
export function investigationSummary(brief: string, result: Result): string {
let { body } = split(brief, result.report);
let paragraph = body.split(/\n{2,}/).find(block =>
block.trim() && !/^\s*(#|[-*+] |\d+[.)] |>|```|~~~|<|\|)/.test(block)
);
return paragraph ? clip(plain(paragraph), SUMMARY_LENGTH) : "";
}

/**
* A completed investigation as a research-shaped child document: its report, an embed for each
* native view, then how the work was done. Raw evidence fills what room the source limit leaves.
*/
export function investigationDocument(
id: string,
brief: string,
result: Result,
): { title: string; source: string } {
if (PROTECTED.test(JSON.stringify(parse(result.report)))) {
throw new ExperimentError(
"protected-report",
"A report cannot embed record-backed components.",
);
}
let { title, lead, body } = split(brief, result.report);
let blocks = [lead, body.trim()];
if (result.views.length > 0) {
blocks.push("## Views");
for (let view of result.views) {
blocks.push(`<Experiment experiment="${id}" view="${view.key}" />`);
}
}
let { environment, checks, limitations } = result.provenance;
if (environment.trim() || checks.length > 0) {
blocks.push("## Method");
if (environment.trim()) blocks.push(reportText(environment));
if (checks.length > 0) blocks.push(list(checks));
}
if (limitations.length > 0) {
blocks.push("## Limitations");
blocks.push(list(limitations));
}
let used = bytes(blocks.join("\n\n"));
let evidence: string[] = [];
for (let item of result.evidence) {
let heading = `### ${reportText(item.title)}`;
let room = SOURCE_BUDGET - used - bytes(heading) - 64;
if (room < 256) break;
let text = bytes(item.text) <= room
? item.text
: `${Buffer.from(item.text).subarray(0, room).toString("utf8").replace(/�$/, "")}\n…`;
let block = `${heading}\n\n${fence(text, LANGUAGES[item.format])}`;
evidence.push(block);
used += bytes(block) + 2;
}
if (evidence.length > 0) blocks.push("## Evidence", ...evidence);
let source = canonical(`${blocks.filter(Boolean).join("\n\n")}\n`);
if ("issues" in source) {
throw new ExperimentError(
"invalid-report",
source.issues.map(issue => issue.message).join("; ").slice(0, 4000),
);
}
return { title, source: source.source };
}
67 changes: 64 additions & 3 deletions apps/server/src/experiments/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async function setup() {
encryptionKey: new Uint8Array(32),
};
let router = new Router();
let lock = { locked: false };
let lock = { locked: false, documents: new Set<string>() };
let runtime = registerExperimentRoutes(router, {
storage,
sessions,
Expand All @@ -72,7 +72,7 @@ async function setup() {
lease: () => lease,
changed: () => {},
context: async () => ({ source: "# Context", revision: 0 }),
canMutate: async () => !lock.locked,
canMutate: async id => !lock.locked && !lock.documents.has(id),
});
async function request(path: string, body?: unknown, cookie?: string, token?: string) {
return (await router.handle(
Expand Down Expand Up @@ -127,7 +127,7 @@ async function setup() {
}

test("work runs on the clicker's own connection; scoped claims publish", async () => {
let { sessions, config, alice, bob, documentId, request } = await setup();
let { sessions, config, alice, bob, documentId, request, lock } = await setup();
let pair = await (await request("/api/connector/pairings", {
repository: "org/repo",
commit: "a".repeat(40),
Expand Down Expand Up @@ -167,6 +167,10 @@ test("work runs on the clicker's own connection; scoped claims publish", async (
expect(claim.input.requester).toBe("bob");
expect(claim.input.authorizer).toBe("alice");
expect((await tool("claim_experiment", { id }, claim.runToken)).result.isError).toBe(true);
let smuggled = await tool("submit_investigation_result", {
result: { ...performance, report: `# Report\n\n<Investigation id="${id}" />\n` },
}, claim.runToken);
expect(error(smuggled)).toStartWith("protected-report");
expect(
(await tool("submit_experiment_result", { result: performance }, claim.runToken)).result
.isError,
Expand All @@ -182,6 +186,63 @@ test("work runs on the clicker's own connection; scoped claims publish", async (
expect(value.state).toBe("completed");
expect(value.result.datasets[0].key).toBe("startup");
expect(value.candidate).toBeUndefined();
expect(value.child).toMatchObject({
title: "Startup comparison",
summary: "Caching reduced median startup time in the captured workload.",
});
let child = value.child.id;
let reached = await request(`/api/documents/${child}/experiments/${id}`, undefined, bob.cookie);
expect((await reached.json()).id).toBe(id);
let listed = await (await request(`/api/documents/${child}/experiments`, undefined, bob.cookie))
.json();
expect(listed.experiments.map((item: { id: string }) => item.id)).toEqual([id]);
expect(
await (await request(`/api/documents/${child}/investigation-provenance`, undefined, bob.cookie))
.json(),
).toEqual({
investigationId: id,
parentChannelId: documentId,
brief: "Measure startup",
viewCount: 1,
startedBy: "bob",
});
expect(
(await request(`/api/documents/${documentId}/investigation-provenance`, undefined, bob.cookie))
.status,
).toBe(404);
// The child's evidence belongs to the parent's record, so the parent's build lock applies.
lock.documents.add(documentId);
for (
let [action, input] of [
["decision", {
id: crypto.randomUUID(),
view: "comparison",
revision: 0,
conclusion: "Use cached startup",
rationale: "",
}],
["placement", { view: "comparison" }],
] as const
) {
let refused = await request(
`/api/documents/${child}/experiments/${id}/${action}`,
input,
bob.cookie,
);
expect(refused.status).toBe(409);
expect((await refused.json()).error).toBe("document-locked");
}
expect(
(await (await request(`/api/documents/${child}/experiments/${id}`, undefined, bob.cookie))
.json()).locked,
).toBe(true);
lock.documents.delete(documentId);
expect((await tool("complete_experiment", { id, generation: claim.generation })).result.isError)
.not.toBe(true);
expect(
(await (await request(`/api/documents/${documentId}/experiments/${id}`, undefined, bob.cookie))
.json()).child.id,
).toBe(child);
await sessions.revoke(
new Request(config.origin, { headers: { cookie: alice.cookie.split(";")[0] } }),
);
Expand Down
Loading
Loading