mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
Bind content projections to the observed reply text
Retain each visible link label so the source content projection cannot attest to different text. Keep decorated-label support with exact destinations and negative coverage. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4f7489b60a
commit
97048b0153
4 files changed
+14
-6
No files matched your search
@@ -66,7 +66,7 @@ describe("neutral decline evidence", () => {
|
||||
const input = native();
|
||||
const body = "Contacts are unavailable. See [Connection outcome](/QA/issues/QA-123#document-outcome).";
|
||||
Object.assign(input.nativeFinalReply, { sourceBody: body, body: "Contacts are unavailable. See QA-123.", origin: "https://fixture.test",
|
||||
content: [{ text: "Contacts are unavailable. See " }, { href: "/QA/issues/QA-123#document-outcome" }, { text: "." }] });
|
||||
content: [{ text: "Contacts are unavailable. See " }, { href: "/QA/issues/QA-123#document-outcome", label: "QA-123" }, { text: "." }] });
|
||||
input.nativeFinalReply.finalEvent.body = body;
|
||||
expect(passes(input)).toBe(true);
|
||||
Object.assign(input.nativeFinalReply, { sourceBody: body.replace("Connection outcome", "PRIVATE_MARKER") });
|
||||
|
||||
@@ -80,11 +80,12 @@ it("binds complete Markdown content and the exact link destination despite a dec
|
||||
const published = { ...run, resultJson: { presentationDecision: { commentId: "selected" } } };
|
||||
const bubble: TaskReplyObservation = { ...observed, anchor: "selected", origin: "https://fixture.test",
|
||||
body: "I couldn't retrieve the pages. See QA-123.",
|
||||
content: [{ text: "I couldn't retrieve the pages. See " }, { href: "https://fixture.test/QA/issues/QA-123#document-outcome" }, { text: "." }] };
|
||||
content: [{ text: "I couldn't retrieve the pages. See " }, { href: "https://fixture.test/QA/issues/QA-123#document-outcome", label: "QA-123" }, { text: "." }] };
|
||||
expect(taskReplyBelongsToRun(bubble, published, "owned-agent", [comment])).toBe(true);
|
||||
const copy = events(); copy[0]!.payload.prpEvent.payload.text = body;
|
||||
expect(taskReplyBelongsToRun({ ...bubble, anchor: "final-run:turn" }, run, "owned-agent", [], copy)).toBe(true);
|
||||
expect(taskReplyBelongsToRun({ ...bubble, body: "Different visible response" }, published, "owned-agent", [comment])).toBe(false);
|
||||
for (const content of [undefined, [{ text: bubble.body }], [...bubble.content!, { text: " extra" }],
|
||||
[{ text: "I couldn't retrieve the pages. See " }, { href: "/other-task" }, { text: "." }]])
|
||||
[{ text: "I couldn't retrieve the pages. See " }, { href: "/other-task", label: "QA-123" }, { text: "." }]])
|
||||
expect(taskReplyBelongsToRun({ ...bubble, content }, published, "owned-agent", [comment])).toBe(false);
|
||||
});
|
||||
@@ -54,13 +54,13 @@ export function uniqueBoundTaskReply(all: TaskReplyObservation[], run: Row, agen
|
||||
|
||||
export function observeTaskReplyElements(elements: Array<HTMLElement | SVGElement>): TaskReplyObservation[] {
|
||||
return elements.map(element => {
|
||||
const content: Array<{ text: string } | { href: string }> = [];
|
||||
const content: Array<{ text: string } | { href: string; label: string }> = [];
|
||||
const walk = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) { content.push({ text: node.textContent ?? "" }); return; }
|
||||
if (!(node instanceof HTMLElement) || node.getAttribute("aria-hidden") === "true"
|
||||
|| getComputedStyle(node).display === "none" || getComputedStyle(node).visibility === "hidden") return;
|
||||
if (node instanceof HTMLAnchorElement) {
|
||||
if (node.getClientRects().length && (node.innerText.trim() || node.getAttribute("aria-label"))) content.push({ href: node.href });
|
||||
if (node.getClientRects().length && node.innerText.trim()) content.push({ href: node.href, label: node.innerText });
|
||||
return;
|
||||
}
|
||||
const block = ["P", "DIV", "BR", "LI", "PRE", "BLOCKQUOTE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
|
||||
export type ReplyContentPart = { text: string } | { href: string };
|
||||
export type ReplyContentPart = { text: string } | { href: string; label?: string };
|
||||
type MarkdownNode = { type: string; value?: string; url?: string; children?: MarkdownNode[] };
|
||||
|
||||
function normalized(parts: ReplyContentPart[], origin: string) {
|
||||
@@ -28,6 +28,13 @@ export function taskReplyContentMatches(
|
||||
observation: { body: string; content?: ReplyContentPart[]; origin?: string },
|
||||
markdown: string,
|
||||
) {
|
||||
if (observation.content) {
|
||||
// Bind the structured projection to the independently observed innerText.
|
||||
// A destination alone cannot attest to arbitrary visible wording.
|
||||
if (observation.content.some(part => "href" in part && (typeof part.label !== "string" || !part.label.trim()))) return false;
|
||||
const displayed = observation.content.map(part => "text" in part ? part.text : part.label!).join("").replace(/\s+/g, " ").trim();
|
||||
if (displayed !== observation.body.replace(/\s+/g, " ").trim()) return false;
|
||||
}
|
||||
const parts: ReplyContentPart[] = [];
|
||||
const inlineLabel = (node: MarkdownNode): boolean => ["text", "inlineCode", "strong", "emphasis"].includes(node.type)
|
||||
&& (node.children ?? []).every(inlineLabel);
|
||||
|
||||
Reference in new issue
Block a user