diff --git a/CHANGELOG.md b/CHANGELOG.md index 858aeef..0edae61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ the section in the pull request that bumps the version: a one-line summary paragraph, then one bullet per change a user or operator would notice. Keep each paragraph and bullet on one line; release pages render line breaks. +## 0.19.1 - 2026-10-01 + +Page claim checks exclude script and stylesheet bodies with HTML closing-tag whitespace or trailing attributes. + +- Keep hidden script and style text out of page evidence when closing tags contain HTML whitespace or trailing attributes, or the fetched page ends inside a raw-text body. +- Preserve visible text in similarly named custom elements and retain existing fetch and evidence limits. + ## 0.19.0 - 2026-09-30 Sys1 can update supported global CLI installations before work starts. diff --git a/package.json b/package.json index 601e77e..26e8063 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hraness/sys1", - "version": "0.19.0", + "version": "0.19.1", "description": "Sys1 gives coding agents tools to review code, check completion claims, and get structured answers from Jev or a local model.", "type": "module", "license": "MIT", diff --git a/src/verify/evidence.ts b/src/verify/evidence.ts index 7f16abf..5ae25a6 100644 --- a/src/verify/evidence.ts +++ b/src/verify/evidence.ts @@ -180,7 +180,10 @@ export async function fetchPage( } const text = new TextDecoder().decode(whole); evidence.ok = true; - evidence.excerpt = text.replace(//gi, " ").replace(//gi, " ") + // Raw-text bodies are not visible evidence. Accept HTML close-tag whitespace + // and discard an unfinished body when the bounded page ends inside it. + evidence.excerpt = text.replace(/])[^>]*>[\s\S]*?(?:<\/script(?=[\t\n\f\r />])[^>]*>|$)/gi, " ") + .replace(/])[^>]*>[\s\S]*?(?:<\/style(?=[\t\n\f\r />])[^>]*>|$)/gi, " ") .replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, EVIDENCE_LIMITS.maxPageBytes); return evidence; } catch { diff --git a/src/version.ts b/src/version.ts index 913368f..86a7e9a 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const SYS1_VERSION = "0.19.0"; +export const SYS1_VERSION = "0.19.1"; diff --git a/test/verify-page-excerpt.test.ts b/test/verify-page-excerpt.test.ts new file mode 100644 index 0000000..7f2cb89 --- /dev/null +++ b/test/verify-page-excerpt.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from "bun:test"; +import { EVIDENCE_LIMITS, fetchPage } from "../src/verify/evidence.ts"; + +for (const tag of ["script", "style"]) { + for (const whitespace of [" ", "\t", "\n", "\r", "\f", " \n\t"]) { + test(`page excerpt excludes ${tag} with close-tag whitespace ${JSON.stringify(whitespace)}`, async () => { + const page = await fetchPage("https://example.com", async () => new Response( + `

Visible before.

<${tag.toUpperCase()} type="text/plain">hidden-evidence-canary

Visible after.

`, + )); + expect(page.ok).toBe(true); + expect(page.excerpt).toBe("Visible before. Visible after."); + }); + } + for (const tail of ["/", " ignored", " ignored=\"value\""]) { + test(`page excerpt retains following text after ${tag} end-tag tail ${tail}`, async () => { + const page = await fetchPage("https://example.com", async () => new Response( + `

Before.

<${tag}>hidden-evidence-canary

After.

`, + )); + expect(page.excerpt).toBe("Before. After."); + }); + } + test(`page excerpt excludes ${tag} body cut off by the byte cap`, async () => { + const page = await fetchPage("https://example.com", async () => new Response( + `

Visible.

<${tag}>` + "hidden-evidence-canary ".repeat(EVIDENCE_LIMITS.maxPageBytes) + ``, + )); + expect(page.excerpt).toBe("Visible."); + }); +} + +test("similarly named custom elements retain their visible text", async () => { + const page = await fetchPage("https://example.com", async () => new Response( + "Visible example.Visible guide.", + )); + expect(page.excerpt).toBe("Visible example. Visible guide."); +});