From 4663f7de0ee7978ee35403726648f4856eb8851b Mon Sep 17 00:00:00 2001 From: kenny yong Date: Wed, 14 Oct 2026 12:46:45 +0800 Subject: [PATCH] Allow canvas-authored pinned on flow case nodes Etherscan Flow now lets a user pin a card so it keeps its x/y through dragging and Tidy Up, and saves that as nodes[].pinned: true. The node schema is additionalProperties: false, so every exported case with a pin failed validation. Add pinned as an optional const-true field reserved for the canvas (the tracer never emits it; absent means unpinned), with a regression test and a note in the output spec and README. Co-Authored-By: Claude Opus 5.5 --- skills/etherscan-flow/README.md | 2 +- skills/etherscan-flow/references/output-spec.md | 1 + skills/etherscan-flow/schema/case.schema.json | 4 ++++ skills/etherscan-flow/schema/test_validate.py | 11 +++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/skills/etherscan-flow/README.md b/skills/etherscan-flow/README.md index 25c7db6..3a7fc6b 100644 --- a/skills/etherscan-flow/README.md +++ b/skills/etherscan-flow/README.md @@ -215,7 +215,7 @@ If you hit a false positive on Claude: report it via `/feedback`, and if you do Every node and edge carries a `chainid`. `hop` counts transfers from the seed, so seed and scope addresses are `hop: 0`. Final edges are chronological by timestamp or block; receipt-log edges sharing one txhash are ordered by `log_index`; node coordinates place hop columns left-to-right with same-hop branches top-to-bottom by first-flow sequence. `_meta.layout` records the ordering and spacing that produced those coordinates. Repeated movements between the same pair collapse into one edge: `txcount` is how many transactions it merges, `txhash` is the earliest, and `txhashes` lists all of them so the canvas can validate each one on-chain. Anything the API did not resolve — a balance that was never needed, an unknown token symbol — is `null` rather than a guess. Financial totals live under `_meta.financials`; structured forensic conclusions live under `_meta.analysis` (`null` for ordinary cases). There are no top-level `financials` or `analysis` keys. -When a user edits Case Findings in Etherscan Flow, the canvas preserves the structured analysis and stores the displayed Markdown at `_meta.ui.findings_markdown`. The tracer itself does not emit this reserved UI field. Saved and exported layouts may also carry non-zero numeric `x` and `y` coordinates. +When a user edits Case Findings in Etherscan Flow, the canvas preserves the structured analysis and stores the displayed Markdown at `_meta.ui.findings_markdown`. The tracer itself does not emit this reserved UI field. Saved and exported layouts may also carry non-zero numeric `x` and `y` coordinates, and `"pinned": true` on a node the user locked in place (also canvas-only; the tracer never emits it). The full contract is [`schema/case.schema.json`](./schema/case.schema.json) (JSON Schema), with a validating example in [`examples/`](./examples/). CI checks the example against the schema on every push, so the documented shape and a real case can't drift apart. diff --git a/skills/etherscan-flow/references/output-spec.md b/skills/etherscan-flow/references/output-spec.md index 7a7ebf5..fe5c1b0 100644 --- a/skills/etherscan-flow/references/output-spec.md +++ b/skills/etherscan-flow/references/output-spec.md @@ -87,6 +87,7 @@ Node `id` values must be short unique alphanumeric strings (2–12 chars, e.g. ` - **`_meta.financials`** is where all financial totals live (Step 3B). There is no top-level `financials` key; the top level is exactly `id`, `name`, `schemaVersion`, `nodes`, `edges`, `_meta`. - **`_meta.analysis`** is `null` for ordinary cases and a required structured forensic conclusion for security cases. `_meta.patterns` remains a compact index of detected patterns; it never replaces the evidence, confidence, competing hypotheses, and limitations in `_meta.analysis`. - **`_meta.layout`** is emitted by the tracer's final ordering pass. It records the chronology strategy, same-transaction ordering, node ordering, direction, and coordinate spacing that produced the non-zero layout; it is separate from user-edited canvas state. +- **Node `pinned` is reserved for Etherscan Flow.** The tracer does not emit it. When a user pins a card on the canvas, the saved or exported case carries `"pinned": true` on that node, and the card keeps its `x`/`y` through dragging and Tidy Up. The field is absent when unpinned — never `false`. - **`_meta.ui` is reserved for Etherscan Flow.** The tracer does not emit it. After a user edits Case Findings, Etherscan Flow stores the displayed Markdown at `_meta.ui.findings_markdown` while preserving `_meta.analysis`. This user-authored Markdown may contain newlines and exceed the tracer's 200-character field limit; the schema caps `findings_markdown` at 1,048,576 Unicode characters (not bytes), while the whole-document byte size is a separate limit the application enforces. Older root `findings`, `_meta.findings_markdown`, and structured `_meta.ui.findings` are migration inputs, not new tracer output fields; a structured `_meta.ui.findings` is still fully sanitized (HTML tags/control chars stripped, each string ≤ 200 chars) — only `findings_markdown` is exempt. ### Edge merging diff --git a/skills/etherscan-flow/schema/case.schema.json b/skills/etherscan-flow/schema/case.schema.json index 15c82bc..baa449a 100644 --- a/skills/etherscan-flow/schema/case.schema.json +++ b/skills/etherscan-flow/schema/case.schema.json @@ -173,6 +173,10 @@ "y": { "type": "number", "description": "Canvas y-coordinate. Tracer output orders nodes within a hop by first-flow chronology at 180-unit intervals; Etherscan Flow may persist a user-edited layout." + }, + "pinned": { + "const": true, + "description": "Reserved for Etherscan Flow; the tracer never emits it. Set when a user pins the card so it keeps its x/y through dragging and Tidy Up. Absent means unpinned; the canvas deletes the field on unpin rather than writing false." } } }, diff --git a/skills/etherscan-flow/schema/test_validate.py b/skills/etherscan-flow/schema/test_validate.py index e67af3e..2af130e 100644 --- a/skills/etherscan-flow/schema/test_validate.py +++ b/skills/etherscan-flow/schema/test_validate.py @@ -56,6 +56,17 @@ def test_layout_is_required_and_persistable(self): nonzero["nodes"][0]["y"] = 80 self.assertEqual([], self.schema_errors(nonzero)) + def test_canvas_pin_is_accepted_only_as_true(self): + pinned = self.case() + pinned["nodes"][0]["pinned"] = True + self.assertEqual([], self.schema_errors(pinned)) + + for bad in (False, "true", 1): + with self.subTest(pinned=bad): + case = self.case() + case["nodes"][0]["pinned"] = bad + self.assertTrue(self.schema_errors(case)) + def test_tiny_plain_decimal_string_is_accepted(self): case = self.case() case["edges"][0]["amount"] = "0.000000000000092695"