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
3 changes: 3 additions & 0 deletions apps/dashboard/app/(main)/insights/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
StatusDot,
Textarea,
} from "@databuddy/ui";
import { ContextUsed } from "../_components/context-used";
import { ExecuteDefinitionAction } from "../_components/investigation-row";

type TimelineItem = InsightByIdResponse["timeline"][number];
Expand Down Expand Up @@ -471,6 +472,8 @@ function InvestigationActivity({
}
/>

<ContextUsed snapshot={outcome.contextSnapshot} />

<NextStep
hideAction={executable}
next={outcome.next}
Expand Down
118 changes: 118 additions & 0 deletions apps/dashboard/app/(main)/insights/_components/context-used.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, expect, it } from "bun:test";
import type { BusinessContext } from "@databuddy/shared/insights";
import { renderToStaticMarkup } from "react-dom/server";
import { ContextUsed } from "./context-used";

const snapshot: BusinessContext = {
capturedAt: "2026-09-08T12:00:00Z",
status: "ready",
issues: [],
sources: [
{
id: "profile-example",
kind: "organization_profile",
origin: "team",
content: "Preparation starts a draft. <script>unsafe()</script>",
observedAt: "2026-09-08T11:00:00Z",
profileVersion: { revision: 3, updatedAt: "2026-09-08T11:00:00Z" },
references: [
{ title: "Report guide", url: "https://example.com/reports" },
],
},
],
};

describe("Business context disclosure", () => {
it("shows supplied text and revision without claiming fact-to-claim attribution", () => {
const html = renderToStaticMarkup(<ContextUsed snapshot={snapshot} />);
expect(html).toContain("Business context");
expect(html).toContain('aria-expanded="false"');
expect(html).toContain("Background available for this update.");
expect(html).toContain(
"does not identify which facts influenced individual claims"
);
expect(html).toContain("Revision 3");
expect(html).toContain('dateTime="2026-09-08T11:00:00Z"');
expect(html).toContain('href="https://example.com/reports"');
expect(html).toContain("Report guide");
expect(html).toContain("Preparation starts a draft.");
expect(html).not.toContain("<script>");
});
it("distinguishes mixed background from named team priorities at the supplied revision", () => {
const html = renderToStaticMarkup(
<ContextUsed
snapshot={{
...snapshot,
sources: [
{
id: "mixed-background",
kind: "organization_profile",
origin: "mixed",
content: "Edited public background",
author: "Edited website background",
observedAt: snapshot.capturedAt,
profileVersion: { revision: 4, updatedAt: snapshot.capturedAt },
},
{
id: "team-context",
kind: "organization_profile",
origin: "team",
content: "Priority: completed downloads",
author: "Team priorities and definitions",
observedAt: snapshot.capturedAt,
profileVersion: { revision: 4, updatedAt: snapshot.capturedAt },
},
],
}}
/>
);
expect(html).toContain("Website background with team edits");
expect(html).toContain("Team priorities and definitions");
expect(html).toContain("Priority: completed downloads");
expect(html.match(/Revision 4/g)).toHaveLength(2);
});
it("does not invent provenance for legacy results", () => {
expect(renderToStaticMarkup(<ContextUsed />)).toBe("");
});
it.each([
"unavailable",
"disabled",
"ready",
"partial",
] as const)("does not claim background was available for an empty %s snapshot", (status) => {
const html = renderToStaticMarkup(
<ContextUsed snapshot={{ ...snapshot, status, sources: [] }} />
);
expect(html).toContain(
status === "unavailable"
? "Business context was unavailable for this update."
: "No business context sources were supplied for this update."
);
expect(html).not.toContain("Background available for this update.");
expect(html).not.toContain("which facts influenced individual claims");
expect(html).not.toContain("Revision");
});
it("does not turn non-web source URLs into clickable links", () => {
const source = snapshot.sources[0];
if (!source) {
throw new Error("Expected source fixture");
}
const html = renderToStaticMarkup(
<ContextUsed
snapshot={{
...snapshot,
sources: [
{
...source,
references: [
{ title: "Untrusted link", url: "javascript:alert(1)" },
],
},
],
}}
/>
);
expect(html).toContain("Untrusted link");
expect(html).not.toContain('href="javascript:');
});
});
120 changes: 120 additions & 0 deletions apps/dashboard/app/(main)/insights/_components/context-used.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

import type {
BusinessContext,
BusinessSource,
} from "@databuddy/shared/insights";
import { formatDateTime } from "@databuddy/ui";
import { Accordion } from "@databuddy/ui/client";

export function ContextUsed({ snapshot }: { snapshot?: BusinessContext }) {
if (!snapshot) {
return null;
}
return (
<Accordion>
<Accordion.Trigger className="bg-transparent px-2">
Business context
</Accordion.Trigger>
<Accordion.Content className="space-y-3 px-2 text-muted-foreground text-xs">
{snapshot.sources.length > 0 && (
<p>
Background available for this update. This snapshot does not
identify which facts influenced individual claims.
</p>
)}
<p>
Captured{" "}
<time dateTime={snapshot.capturedAt}>
{formatDateTime(snapshot.capturedAt)}
</time>
</p>
{snapshot.status === "partial" && (
<p>Some context was unavailable or omitted.</p>
)}
{snapshot.sources.length === 0 && (
<p>
{snapshot.status === "unavailable"
? "Business context was unavailable for this update."
: "No business context sources were supplied for this update."}
</p>
)}
<ul className="space-y-4">
{snapshot.sources.map((source) => (
<li className="min-w-0 space-y-1" key={source.id}>
<p className="font-medium text-foreground">
{sourceName(source)}
</p>
{source.kind === "organization_profile" && source.author && (
<p>{source.author}</p>
)}
<p>
{source.profileVersion
? `Revision ${source.profileVersion.revision} · Saved `
: "Recorded "}
<time
dateTime={
source.profileVersion?.updatedAt ?? source.observedAt
}
>
{formatDateTime(
source.profileVersion?.updatedAt ?? source.observedAt
)}
</time>
</p>
<p className="whitespace-pre-wrap break-words text-foreground/80 leading-relaxed">
{source.content}
</p>
{source.url && <SourceLink url={source.url} title={source.url} />}
{Boolean(source.references?.length) && (
<div className="space-y-1 pt-1">
<p>Source links</p>
<ul className="space-y-1">
{source.references?.map((reference) => (
<li key={reference.url}>
<SourceLink {...reference} />
</li>
))}
</ul>
</div>
)}
</li>
))}
</ul>
</Accordion.Content>
</Accordion>
);
}

function sourceName(source: BusinessSource) {
if (source.kind === "organization_profile") {
return source.origin === "website"
? "Organization brief · Website background"
: source.origin === "mixed"
? "Organization brief · Website background with team edits"
: source.origin === "team"
? "Organization brief · Team supplied or edited"
: "Organization brief";
}
return source.kind === "team_reply"
? `Team reply${source.author ? ` · ${source.author}` : ""}`
: "Website excerpt";
}

function SourceLink({ url, title }: { url: string; title: string }) {
const protocol = new URL(url).protocol;
if (protocol !== "https:" && protocol !== "http:") {
return <span className="break-all">{title || url}</span>;
}
return (
<a
className="break-all underline underline-offset-2 hover:text-foreground"
href={url}
rel="noopener noreferrer"
target="_blank"
title={url}
>
{title || url}
</a>
);
}
25 changes: 14 additions & 11 deletions apps/insights/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1417,17 +1417,20 @@ export async function runInsightAgent(
repository: input.githubRepository,
investigationObjective: input.investigationObjective,
evidence: input.evidence,
history: input.history.map((item) =>
item.kind === "investigation"
? {
asOf: item.asOf,
evidence: item.evidence,
kind: item.kind,
outcome: item.outcome,
signal: promptSignal(item.signal),
}
: item
),
history: input.history.map((item) => {
if (item.kind !== "investigation") {
return item;
}
// Prior snapshots remain inspectable history, not fresh model context.
const { contextSnapshot: _snapshot, ...outcome } = item.outcome;
return {
asOf: item.asOf,
evidence: item.evidence,
kind: item.kind,
outcome,
signal: promptSignal(item.signal),
};
}),
otherOpenWork: input.otherOpenWork,
...(input.request
? {
Expand Down
39 changes: 39 additions & 0 deletions apps/insights/src/business-context-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from "@databuddy/ai/lib/business-context";
import {
loadWebsiteBusinessProfile,
organizationProfileContext,
recallWebsiteBusinessContext,
} from "./business-context";
import { planInvestigationsWithBusinessContext } from "./generation";
Expand Down Expand Up @@ -60,6 +61,44 @@ const profile: BusinessContext = {
};

describe("freezing investigation business context", () => {
it("retains the captured canonical revision when the saved profile changes after freezing", () => {
const reference = { title: "Report guide", url: "https://example.com/reports" };
const saved = {
content: "Preparation starts a draft.",
origin: "team" as const,
revision: 3,
updatedAt: "2026-07-11T11:00:00.000Z",
updatedBy: "example-editor",
sourceWebsiteId: null,
sources: [reference],
};
const context = organizationProfileContext(
saved,
input.organizationId,
new Date(input.asOf)
);
const stored = JSON.stringify({
asOf: input.asOf,
reason: "manual",
businessScope,
candidates: [{ ...candidates[0], businessContext: context }],
});
saved.revision = 4;
saved.content = "New operational priority.";
reference.title = "Changed source";
const frozen = parseFrozenInvestigationPlan(
JSON.parse(stored),
"manual",
businessScope
);
expect(frozen.candidates[0]?.businessContext?.sources[0]).toMatchObject({
content: "Preparation starts a draft.",
profileVersion: { revision: 3, updatedAt: "2026-07-11T11:00:00.000Z" },
references: [
{ title: "Report guide", url: "https://example.com/reports" },
],
});
});
it("loads shared context once and recalls each exact subject before freezing", async () => {
let profileReads = 0;
const recalled: string[] = [];
Expand Down
Loading
Loading