From dcfed62990c8f71d717fc14124ff578052507563 Mon Sep 17 00:00:00 2001 From: Ivan Kuznetsov Date: Fri, 4 Sep 2026 17:53:09 +0100 Subject: [PATCH] fix(screenote): keep snapshot pages screen-specific --- plugin-surfaces.lock.json | 24 ++++----- plugins/screenote/CHANGELOG.md | 5 ++ plugins/screenote/references/cli.md | 8 +-- plugins/screenote/scripts/screenote_flow.py | 6 +++ plugins/screenote/skills/screenote/SKILL.md | 4 +- plugins/screenote/skills/snapshot/SKILL.md | 7 ++- tests/test_screenote_cli_contract.py | 58 +++++++++++++++++++++ wiki/architecture.md | 4 +- wiki/gaps.md | 3 ++ 9 files changed, 100 insertions(+), 19 deletions(-) diff --git a/plugin-surfaces.lock.json b/plugin-surfaces.lock.json index e2a3520..e87fb67 100644 --- a/plugin-surfaces.lock.json +++ b/plugin-surfaces.lock.json @@ -856,8 +856,8 @@ "version": "3.2.0", "canonical": { "skills/screenote/SKILL.md": { - "sha256": "2944db045534e4d6f45f9637d26c0b117fb96e3595c584cdf4155a635c875782", - "semantic_sha256": "08f78714c649329f08575cb905597b2e4687bfeb55617ea7c7a22f08295c2209", + "sha256": "0dd17ee55e234d8e0e92db3c9d991a1f69a5837d6af785e07dcad19b1df23d8a", + "semantic_sha256": "9f8b702e00b4e291801d173583f745e72f64e7f85786debe5d396eafdfcd5f0a", "sections": { "1:screenote — one-page visual review": "6860d0df6138cb5f476bd78c6be5a9986d11aa8022f56510b21c9876e3b66acb", "2:parse the request": "07af83c395625a98b29917f770bfd2628eb303cec19c667f92ba8bdc6de46ce0", @@ -865,18 +865,18 @@ "2:establish the cli and project": "3e721f75c2d0227fb1522d224de9d573d31ad57480f33cfbece0e6b8d5cf0e2b", "2:existing-image upload mode": "f1b596ac83c23e33fc9fe64e1be0e81aa6dcf8229a1678cd638c01416c37f8d2", "2:browser capture and upload mode": "d9887b3a12436b01bb61c4c14f78e1438502b29851eec0479d44cf55e5102696", - "2:build and publish one logical version": "f829329359547291e730570a7c9513752ea128b46fb035efd66d0bd790d046d6", + "2:build and publish one logical version": "9f43d88d0f4360bb829fa2fce5204baf6dd127750092d76f66cb088b11f82327", "2:report and clean up": "6e2a6f1cb431fd682f1faad3d478baafd331075dc9c6e81e1b39fb5841f3f31c" } }, "skills/snapshot/SKILL.md": { - "sha256": "a07233be927a686d5d77b6b5276b44ee65c6b878b8513dcaa2f7ec78dea82e8b", - "semantic_sha256": "8932abdb3a46b21fd5f7a3244fe40b33623b7c069d08f05cf8afe0a421383c4b", + "sha256": "2117830d92dd06273ecd8d02afca4ab01722b4a345b8ad65f243f59315d76e6c", + "semantic_sha256": "981e7eed7dea040b7337f53da738bdb754ca0cfb4f833f13b42198385f66d560", "sections": { "1:snapshot — multi-page visual review": "6be3486148325dcc124469e4eff8988f00b33f0e8778d2d4fe7fa13e40cc1026", "2:preflight": "4a0dd546789198a62058792ec0866f08ac3e9c65d35fc3d856b44a42479c5629", "2:discover and confirm routes": "5f514b67e742e1beac6d215cb8802e081f26eae46fd07c9698785f188bbfc257", - "2:capture and publish": "61216542ce17a118b8b6feaf46849fa2b8c20d1235270c7dc5fdb921cb4b81df" + "2:capture and publish": "cf02f92c1b7091fbcb3fbf527b424350266012cabeb1b038f1591b27df506a4f" } }, "skills/feedback/SKILL.md": { @@ -893,7 +893,7 @@ "resources": { "references": { "exists": true, - "sha256": "4c65cfd4dbef0720cfc33cfc893e02acbfbc09f4d674b35755be1f352ba559d5", + "sha256": "bdc193fa70a54038a311bd3416d3e0c833d82117cc5fe7333314d93f2fd90343", "files": [ "references/cli.md", "references/workflows.json" @@ -916,14 +916,14 @@ }, "scripts/screenote_flow.py": { "exists": true, - "sha256": "e7183fe7a22ab633bad4bd7ea21655daf214f37c533614eee53c8894c38e6ae7" + "sha256": "7f017523318da1ae7da795b9296b15804a0ddb0e106508e2cd62d7f1a9a780f1" } }, "adapters": { "pi/skills/screenote/SKILL.md": { "sha256": "6acc53b130d691fe0fe3288e2079904d52bbb76065b6620524cdcbad7f03be3f", "canonical": "skills/screenote/SKILL.md", - "canonical_semantic_sha256": "08f78714c649329f08575cb905597b2e4687bfeb55617ea7c7a22f08295c2209", + "canonical_semantic_sha256": "9f8b702e00b4e291801d173583f745e72f64e7f85786debe5d396eafdfcd5f0a", "overlays": [ "frontmatter", "invocation", @@ -933,7 +933,7 @@ "openclaw/skills/screenote/SKILL.md": { "sha256": "a8e0378d09299bc70297800080e348ad928ea914327b3a761ec5804a6a12f018", "canonical": "skills/screenote/SKILL.md", - "canonical_semantic_sha256": "08f78714c649329f08575cb905597b2e4687bfeb55617ea7c7a22f08295c2209", + "canonical_semantic_sha256": "9f8b702e00b4e291801d173583f745e72f64e7f85786debe5d396eafdfcd5f0a", "overlays": [ "frontmatter", "invocation", @@ -943,7 +943,7 @@ "pi/skills/snapshot/SKILL.md": { "sha256": "c935637fe16765d450c68e1bcf60123d2f2af8e1cc362d39838286b2a52ce931", "canonical": "skills/snapshot/SKILL.md", - "canonical_semantic_sha256": "8932abdb3a46b21fd5f7a3244fe40b33623b7c069d08f05cf8afe0a421383c4b", + "canonical_semantic_sha256": "981e7eed7dea040b7337f53da738bdb754ca0cfb4f833f13b42198385f66d560", "overlays": [ "frontmatter", "invocation", @@ -953,7 +953,7 @@ "openclaw/skills/snapshot/SKILL.md": { "sha256": "adebbedd30e55d544a8600fd6b89b4a3aeb70a5549a9fb65f5e6f7c186aed94e", "canonical": "skills/snapshot/SKILL.md", - "canonical_semantic_sha256": "8932abdb3a46b21fd5f7a3244fe40b33623b7c069d08f05cf8afe0a421383c4b", + "canonical_semantic_sha256": "981e7eed7dea040b7337f53da738bdb754ca0cfb4f833f13b42198385f66d560", "overlays": [ "frontmatter", "invocation", diff --git a/plugins/screenote/CHANGELOG.md b/plugins/screenote/CHANGELOG.md index 2724b23..01ebb9b 100644 --- a/plugins/screenote/CHANGELOG.md +++ b/plugins/screenote/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to the Screenote plugin are documented here. ## [Unreleased] +### Fixed + +- Reject snapshot manifests that put several distinct screens under one Page, + preventing those screens from appearing as versions of each other. + ## [3.2.0] - 2026-09-02 ### Added diff --git a/plugins/screenote/references/cli.md b/plugins/screenote/references/cli.md index b2f61bd..bc5de35 100644 --- a/plugins/screenote/references/cli.md +++ b/plugins/screenote/references/cli.md @@ -187,9 +187,11 @@ screenote_flow.py prepare-snapshot-manifest \ The helper requires 1-100 new or prepared mode-`0600` image files directly beneath the private directory and writes a new mode-`0600` `snapshot.json`. Viewport variants of one logical screen must repeat the exact same `page` and -`title`; only `viewport` and `file` differ. It rejects path escapes, symlinks, -missing files, duplicate `(page, title, viewport)` tuples, and invalid manifest -metadata. +`title`; only `viewport` and `file` differ. One case-insensitive Page +identity may name only that one screen group in a manifest because later +capture runs, not neighboring screens, become Page versions. It rejects path +escapes, symlinks, missing files, duplicate `(page, title, viewport)` tuples, +multiple screen groups under one Page, and invalid manifest metadata. Publish the whole manifest once: diff --git a/plugins/screenote/scripts/screenote_flow.py b/plugins/screenote/scripts/screenote_flow.py index 5f4e443..a246edf 100755 --- a/plugins/screenote/scripts/screenote_flow.py +++ b/plugins/screenote/scripts/screenote_flow.py @@ -267,6 +267,7 @@ def create_snapshot_manifest( normalized_entries: list[dict[str, str]] = [] seen: set[tuple[str, str, str]] = set() + page_groups: dict[str, tuple[str, str]] = {} for entry in entries: page = str(entry.get("page", "")).strip() title = str(entry.get("title", "")).strip() @@ -292,6 +293,11 @@ def create_snapshot_manifest( if key in seen: raise CaptureSafetyError("viewport must be unique within each page and title group") seen.add(key) + page_key = page.lower() + group_key = (page, title) + if page_key in page_groups and page_groups[page_key] != group_key: + raise CaptureSafetyError("each snapshot page must identify one logical screen") + page_groups[page_key] = group_key normalized_entries.append({"page": page, "title": title, "file": filename, "viewport": viewport}) body = json.dumps( diff --git a/plugins/screenote/skills/screenote/SKILL.md b/plugins/screenote/skills/screenote/SKILL.md index 8b3e801..d1efd3d 100644 --- a/plugins/screenote/skills/screenote/SKILL.md +++ b/plugins/screenote/skills/screenote/SKILL.md @@ -161,7 +161,9 @@ argv element and repeat `--entry` once per image: Require exit zero and parse its complete JSON. The helper writes a new mode-`0600` `snapshot.json`, rejects missing/private-path escapes, duplicate -`(page, title, viewport)` tuples, invalid metadata, and more than 100 images. +`(page, title, viewport)` tuples, multiple screen groups under one +case-insensitive Page identity, invalid metadata, and more than 100 images. A +Page identifies one stable screen across runs, not a category within a run. For one screen, every viewport entry must repeat the exact same page and title. Publish exactly once: diff --git a/plugins/screenote/skills/snapshot/SKILL.md b/plugins/screenote/skills/snapshot/SKILL.md index 165c914..2a15f18 100644 --- a/plugins/screenote/skills/snapshot/SKILL.md +++ b/plugins/screenote/skills/snapshot/SKILL.md @@ -92,8 +92,11 @@ entry arguments: ``` Require exit zero and inspect the returned manifest path. The helper rejects -invalid/private-path entries, duplicate `(page, title, viewport)` tuples, and -more than 100 images. Publish the complete route/viewport matrix exactly once: +invalid/private-path entries, duplicate `(page, title, viewport)` tuples, +multiple screen groups under one case-insensitive Page identity, and more than +100 images. A Page is one stable screen, normally the normalized route; it is +not a category for several screens. Publish the complete route/viewport matrix +exactly once: ```text ../../scripts/screenote-cli.sh --project PROJECT_ID snapshot --manifest PRIVATE_MANIFEST --wait 2m diff --git a/tests/test_screenote_cli_contract.py b/tests/test_screenote_cli_contract.py index 6046f40..b6d0baa 100644 --- a/tests/test_screenote_cli_contract.py +++ b/tests/test_screenote_cli_contract.py @@ -773,6 +773,64 @@ def test_snapshot_manifest_helper_preserves_logical_viewport_groups(self): name="other.json", ) + def test_snapshot_manifest_helper_rejects_multiple_screens_for_one_page(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + private = create_private_directory(Path(temporary.name)) + create_private_file(private, "task-board.png") + create_private_file(private, "agent-status.png") + + with self.assertRaisesRegex(CaptureSafetyError, "one logical screen"): + create_snapshot_manifest( + private, + git_commit="abc1234", + taken_at="2026-07-10T10:00:00Z", + entries=[ + { + "page": "Hive Web", + "title": "Task board", + "file": "task-board.png", + "viewport": "desktop", + }, + { + "page": "hive web", + "title": "Agent status", + "file": "agent-status.png", + "viewport": "desktop", + }, + ], + ) + + def test_snapshot_manifest_helper_uses_server_page_case_normalization(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + private = create_private_directory(Path(temporary.name)) + create_private_file(private, "street.png") + create_private_file(private, "capital-street.png") + + manifest_path = create_snapshot_manifest( + private, + git_commit="abc1234", + taken_at="2026-07-10T10:00:00Z", + entries=[ + { + "page": "Straße", + "title": "German street", + "file": "street.png", + "viewport": "desktop", + }, + { + "page": "STRASSE", + "title": "Capital street", + "file": "capital-street.png", + "viewport": "desktop", + }, + ], + ) + + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + self.assertEqual(["Straße", "STRASSE"], [entry["page"] for entry in manifest["images"]]) + def test_capture_targets_must_be_safe_http_urls(self): self.assertEqual("https://example.test/login?q=one", validate_http_url("https://example.test/login?q=one")) for unsafe in ( diff --git a/wiki/architecture.md b/wiki/architecture.md index 220edc3..2a32451 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -39,7 +39,9 @@ collections, pagination, identifiers, private image paths, and manifest-backed viewport groups before canonical skills act on them. Browser captures and explicit existing images publish through one resumable snapshot per logical screen, with desktop, tablet, and mobile represented as child variants rather -than separate versions. Feedback retrieval exports annotation crops plus root +than separate versions. It also enforces one stable Page identity per logical +screen within a capture run, so neighboring screens cannot be published as +versions of one Page. Feedback retrieval exports annotation crops plus root and reply attachments into one private tree. Image replies remain an optional flag on the existing comment tuple, require explicit file intent, and fail closed instead of silently creating a text-only comment. diff --git a/wiki/gaps.md b/wiki/gaps.md index c459e73..a922291 100644 --- a/wiki/gaps.md +++ b/wiki/gaps.md @@ -6,6 +6,9 @@ knowledge remains authoritative here. - Automatic Screenote annotation resolution remains deferred until the mutation is explicitly approved in the CLI contract. +- The Screenote helper now rejects multiple logical screens under one Page. + Repairing already-published malformed snapshots remains a server/data + operation and is outside the plugin package. - LLM Wiki's standalone and four-host marketplace packages intentionally use separate version lines (`0.1.x` upstream runtime and `0.3.x` consent-gated marketplace package). A marketplace release must preserve the consent and