From 4700da320d9e6a2bfe705b43edcefc0e87ec9f7b Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Mon, 11 May 2026 12:12:37 +0200 Subject: [PATCH 1/2] Format: quantize coordinates to integers on save (#20) --- grafli/format.py | 37 ++++++++----- tests/test_format.py | 128 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 145 insertions(+), 20 deletions(-) diff --git a/grafli/format.py b/grafli/format.py index 349a619..645102a 100644 --- a/grafli/format.py +++ b/grafli/format.py @@ -432,11 +432,22 @@ def parse_file(path: str) -> Board: # ── Serializer ────────────────────────────────────────────────── +def _q(value: float) -> int: + """Quantize a coordinate / size to integer pixels. + + Why: full float precision in saved files produces noisy diffs (~14 + digits per moved element) that obscure real edits. Integer pixels + are visually indistinguishable in the UI but keep diffs readable + and round-trips byte-stable. + """ + return round(value) + + def _serialize_box(box: Box) -> str: - x = int(box.x) if box.x == int(box.x) else box.x - y = int(box.y) if box.y == int(box.y) else box.y - w = int(box.w) if box.w == int(box.w) else box.w - h = int(box.h) if box.h == int(box.h) else box.h + x = _q(box.x) + y = _q(box.y) + w = _q(box.w) + h = _q(box.h) escaped_label = box.label.replace("\n", "\\n") s = f'@ box {box.id} "{escaped_label}" {x},{y} {w}x{h}' if box.color: @@ -466,9 +477,9 @@ def _serialize_arrow(arrow: Arrow) -> str: base = f"@ arrow {arrow.from_id} {op} {arrow.to_id}" if arrow.label: base += f' "{arrow.label}"' - if arrow.label_dx or arrow.label_dy: - dx = int(arrow.label_dx) if arrow.label_dx == int(arrow.label_dx) else arrow.label_dx - dy = int(arrow.label_dy) if arrow.label_dy == int(arrow.label_dy) else arrow.label_dy + dx = _q(arrow.label_dx) + dy = _q(arrow.label_dy) + if dx or dy: base += f" @{dx},{dy}" if arrow.style: base += f" !{arrow.style}" @@ -480,8 +491,8 @@ def _serialize_arrow(arrow: Arrow) -> str: def _serialize_note(note: Note) -> str: - x = int(note.x) if note.x == int(note.x) else note.x - y = int(note.y) if note.y == int(note.y) else note.y + x = _q(note.x) + y = _q(note.y) use_block = note.block_text or '"' in note.text if use_block: parts = [f'@ note {note.id} {x},{y} """'] @@ -520,10 +531,10 @@ def _serialize_note(note: Note) -> str: def _serialize_image(image: Image) -> str: - x = int(image.x) if image.x == int(image.x) else image.x - y = int(image.y) if image.y == int(image.y) else image.y - w = int(image.w) if image.w == int(image.w) else image.w - h = int(image.h) if image.h == int(image.h) else image.h + x = _q(image.x) + y = _q(image.y) + w = _q(image.w) + h = _q(image.h) s = f'@ image {image.id} "{image.image_path}" {x},{y} {w}x{h}' if image.parent: s += f" >{image.parent}" diff --git a/tests/test_format.py b/tests/test_format.py index fe14e99..e05b1f5 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,5 +1,6 @@ """Tests for grafli.format — .grafli file parsing and serialization.""" +import re import tempfile from pathlib import Path @@ -123,14 +124,20 @@ def test_box_by_id_missing(): def test_float_coordinates(): + """Float coords parse losslessly but serialize quantized to integers. + + Issue #20: the auto-save must not leak ~14 digits of float noise into + diffs. Quantization on save makes drags produce 1-line diffs.""" text = '@ box f "Float" 10.5,20.3 100.0x50.0\n' board = parse(text) + # parser preserves the input precision assert board.boxes[0].x == 10.5 assert board.boxes[0].y == 20.3 - # integer-like floats should serialize without decimals - assert "100x50" in serialize(board) - # true floats should keep decimals - assert "10.5,20.3" in serialize(board) + out = serialize(board) + # integer-like floats serialize without decimals + assert "100x50" in out + # fractional coords round to integers (no decimals reach the file) + assert "." not in out.split('"Float"')[1].split('\n')[0] def test_negative_coordinates(): @@ -1169,11 +1176,11 @@ def test_arrow_label_offset_zero_omitted(): def test_arrow_all_fields_with_offset_roundtrip(): - text = '@ arrow a <-> b "data" @-5,12.5 !dashed ~xxlarge # check latency\n' + text = '@ arrow a <-> b "data" @-5,12 !dashed ~xxlarge # check latency\n' board = parse(text) arrow = board.arrows[0] assert arrow.label_dx == -5.0 - assert arrow.label_dy == 12.5 + assert arrow.label_dy == 12.0 assert arrow.style == "dashed" assert arrow.textsize == "xxlarge" assert arrow.annotation == "check latency" @@ -1181,7 +1188,7 @@ def test_arrow_all_fields_with_offset_roundtrip(): assert arrow.head_to is True # Annotation is parsed but not serialized result = serialize(board) - assert '@ arrow a <-> b "data" @-5,12.5 !dashed ~xxlarge\n' in result + assert '@ arrow a <-> b "data" @-5,12 !dashed ~xxlarge\n' in result def test_arrow_label_offset_negative(): @@ -1427,3 +1434,110 @@ def test_merge_handles_new_box_added_externally(): merged = merge_box_positions(new_disk, prev_disk, in_memory) new_b = next(b for b in merged.boxes if b.id == "b") assert (new_b.x, new_b.y) == (300, 300) + + +# ── Coordinate quantization on save (issue #20) ─────────────────── + + +def _no_decimals_in_coords(text: str) -> bool: + """No fractional coordinates in serialized output. + + Strips quoted string content from each @ line first so decimals in + labels (e.g. "ratio > 2.0") don't trip the check — we only care + about coordinate / size tokens like 12.5 in `100.5,200`. + """ + for line in text.splitlines(): + if not line.startswith("@"): + continue + outside_quotes = re.sub(r'"[^"]*"', "", line) + if re.search(r"\d+\.\d+", outside_quotes): + return False + return True + + +def test_serialize_quantizes_box_floats(): + """Issue #20: full-precision floats from drags are rounded on save.""" + board = Board() + board.add_box(Box( + id="b1", label="X", + x=1476.537054409133, y=217.99831588774094, + w=160.0, h=89.5001, + )) + out = serialize(board) + assert "1477,218" in out + assert "160x90" in out # 89.5 → 90 via banker's rounding + assert _no_decimals_in_coords(out) + + +def test_serialize_quantizes_note_floats(): + board = Board() + board.add_note(Note(id="n1", x=300.72953746, y=-108.89470385, text="hi")) + out = serialize(board) + assert "301,-109" in out + assert _no_decimals_in_coords(out) + + +def test_serialize_quantizes_image_floats(): + from grafli.format import Image + board = Board() + board.add_image(Image( + id="img1", image_path="x.png", + x=10.4, y=20.6, w=100.49, h=50.51, + )) + out = serialize(board) + assert "10,21" in out + assert "100x51" in out + assert _no_decimals_in_coords(out) + + +def test_serialize_quantizes_arrow_label_offset(): + board = Board() + board.add_arrow(Arrow( + from_id="a", to_id="b", label="x", + label_dx=10.7, label_dy=-20.3, + )) + out = serialize(board) + assert "@11,-20" in out + assert _no_decimals_in_coords(out) + + +def test_serialize_drops_offset_when_both_round_to_zero(): + """A label_dx/dy that rounds to (0, 0) is omitted entirely.""" + board = Board() + board.add_arrow(Arrow( + from_id="a", to_id="b", label="x", + label_dx=0.3, label_dy=-0.4, + )) + out = serialize(board) + assert "@0,0" not in out + assert "@-0,0" not in out + + +def test_serialize_no_float_noise_in_showcase(): + """Round-trip on the showcase example produces no float noise.""" + from pathlib import Path + src = Path(__file__).parent.parent / "examples" / "showcase.grafli" + if not src.exists(): + return # examples are optional in some checkouts + board = parse_file(str(src)) + out = serialize(board) + assert _no_decimals_in_coords(out), \ + "Serialized showcase still contains fractional coords" + + +def test_double_roundtrip_byte_stable(): + """parse → serialize → parse → serialize is byte-stable. + + The first serialize is allowed to quantize; the second must produce + an identical byte sequence.""" + text = ( + '#!grafli v1\n' + '@ box auth "Auth" 1476.537054409133,217.99831588774094 160x89.5\n' + '@ note n1 -300.72,-108.89 "hi"\n' + '@ image img1 "p.png" 10.4,20.6 100.49x50.51\n' + '@ arrow auth -> auth "loop" @10.7,-20.3\n' + ) + once = serialize(parse(text)) + twice = serialize(parse(once)) + assert once == twice + assert _no_decimals_in_coords(once) From 898fce60e3d5016587f9bad32013fff31055ed14 Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Mon, 11 May 2026 12:12:42 +0200 Subject: [PATCH 2/2] Examples: re-save with quantized coords (#20) --- examples/architecture.grafli | 68 ++++++++++++++++----------------- examples/oauth-callback.grafli | 12 +++--- examples/showcase.grafli | 68 ++++++++++++++++----------------- examples/skill-explained.grafli | 18 ++++----- 4 files changed, 83 insertions(+), 83 deletions(-) diff --git a/examples/architecture.grafli b/examples/architecture.grafli index 00ad732..a09f0ab 100644 --- a/examples/architecture.grafli +++ b/examples/architecture.grafli @@ -5,33 +5,33 @@ # nesting, child text notes, block text, markdown resources, and sub-graflis. # --- Title / legend --- -@ note title -142.64805769219618,-93.30944319793657 "Architecture explanation demo" ~xlarge -@ note legend -335.4432297006256,26.190871260717813 "Edge label prefixes render as chips and color the edge: call:, data:, event:, state:, step:, verify:, owns:, depends:, risk:, note:" ~small +@ note title -143,-93 "Architecture explanation demo" ~xlarge +@ note legend -335,26 "Edge label prefixes render as chips and color the edge: call:, data:, event:, state:, step:, verify:, owns:, depends:, risk:, note:" ~small # --- Visible nesting containers --- -@ box frontend_layer "Frontend clients" 10.216128384960427,101.7784935756502 500x230 %secondary ^topleft ~small !flat -@ box api_layer "API boundary" 684.9675628779783,94.83264817843568 500x230 %primary ^topleft ~small !flat -@ box core_layer "Core domain services" 60.56713475112224,638.0582416010043 760x360 %tertiary ^topleft ~small !flat -@ box data_layer "Data and external stores" 914.9518738695019,545.2624693673604 560x360 %subtle ^topleft ~small !flat -@ box observability_layer "Verification / operations" 6.176285612556256,1057.2802719016136 760x280 %muted ^topleft ~small !flat +@ box frontend_layer "Frontend clients" 10,102 500x230 %secondary ^topleft ~small !flat +@ box api_layer "API boundary" 685,95 500x230 %primary ^topleft ~small !flat +@ box core_layer "Core domain services" 61,638 760x360 %tertiary ^topleft ~small !flat +@ box data_layer "Data and external stores" 915,545 560x360 %subtle ^topleft ~small !flat +@ box observability_layer "Verification / operations" 6,1057 760x280 %muted ^topleft ~small !flat # --- Frontend children --- -@ box web "Web App\nReact + Next.js" 572.12552841975,-85.54731796255707 190x80 %secondary -@ box mobile "Mobile App\nReact Native" -236.37555387268583,176.75070675159537 190x80 %secondary -@ note n_frontend_q 50.21612838496043,266.7784935756502 "Q: Should admin UI bypass mobile-oriented caching?" ~small >frontend_layer +@ box web "Web App\nReact + Next.js" 572,-86 190x80 %secondary +@ box mobile "Mobile App\nReact Native" -236,177 190x80 %secondary +@ note n_frontend_q 50,267 "Q: Should admin UI bypass mobile-oriented caching?" ~small >frontend_layer # --- API children --- -@ box gateway "API Gateway\nrate limits + auth" 170.21612838496043,171.7784935756502 200x90 %primary >frontend_layer -@ box graphql "GraphQL Federation\nschema stitching" 802.0935549525444,151.9225791656238 200x90 %primary &architecture-res/graphql.md >api_layer -@ note n_api_task 830.3447930125569,268.56281561682783 "T: Add p95 latency budget to gateway -> graphql edge" ~small >api_layer +@ box gateway "API Gateway\nrate limits + auth" 170,172 200x90 %primary >frontend_layer +@ box graphql "GraphQL Federation\nschema stitching" 802,152 200x90 %primary &architecture-res/graphql.md >api_layer +@ note n_api_task 830,269 "T: Add p95 latency budget to gateway -> graphql edge" ~small >api_layer # --- Core service children --- @ box auth "Auth Service\nOAuth2 + JWT" 0,400 200x85 %tertiary @ box users "User Profile\nService" 240,400 180x85 %tertiary -@ box orders "Order Processing\n& Fulfillment" 485.17863020914706,414.05558603437254 210x85 %tertiary &architecture-res/orders-flow.grafli -@ box catalog "Product Catalog\nService" 889.4722050150008,414.0502305973303 210x85 %tertiary +@ box orders "Order Processing\n& Fulfillment" 485,414 210x85 %tertiary &architecture-res/orders-flow.grafli +@ box catalog "Product Catalog\nService" 889,414 210x85 %tertiary -@ note n_orders_code -455.416501911354,363.81958174053847 """ +@ note n_orders_code -455,364 """ code: fn: placeOrder(req) -> OrderId pre: req.user is authenticated @@ -45,20 +45,20 @@ verify: test_order_happy_path @tests/orders_test.py:41 risk: reservation leak if payment timeout is not compensated """ ~small !mono -@ note n_core_discussion 115.63943407462125,897.5439704114971 "AI: The graph shows service relationships.\nReviewer: The code note keeps order internals local.\nAI: Deeper order flow is linked as a sub-grafli." ~small >core_layer +@ note n_core_discussion 116,898 "AI: The graph shows service relationships.\nReviewer: The code note keeps order internals local.\nAI: Deeper order flow is linked as a sub-grafli." ~small >core_layer # --- Data layer children --- -@ box redis "Redis\nsessions/cache" -172.66632124352293,694.1818652849742 180x85 %subtle -@ box postgres "PostgreSQL\nprimary store" 85.34646327677254,711.4393552051313 200x85 %subtle >core_layer -@ box queue "Kafka\norder events" 489.0295519305754,731.7723570382295 180x85 %subtle >core_layer -@ box search "Search Index\nElasticsearch" 1110.8393706318195,697.8052904232359 200x85 %subtle >data_layer -@ note n_data_note 954.9518738695019,875.2624693673604 "Plain child notes move with their parent box; use them for local context." ~small >data_layer +@ box redis "Redis\nsessions/cache" -173,694 180x85 %subtle +@ box postgres "PostgreSQL\nprimary store" 85,711 200x85 %subtle >core_layer +@ box queue "Kafka\norder events" 489,732 180x85 %subtle >core_layer +@ box search "Search Index\nElasticsearch" 1111,698 200x85 %subtle >data_layer +@ note n_data_note 955,875 "Plain child notes move with their parent box; use them for local context." ~small >data_layer # --- Verification / operations children --- -@ box ci "CI Pipeline\nGitHub Actions" 800.6491973589921,1137.5854113154567 200x85 %muted -@ box tests "Integration Tests\ncontract + flow" 59.55739921668311,1130.5155290241248 210x85 %muted >observability_layer -@ box monitor "Prometheus + Grafana\nruntime signals" 368.0875138889694,1195.3690436252004 200x85 %muted >observability_layer -@ note n_verify_code 46.176285612556256,1242.2802719016136 "code:\nfn: verifyChange(pr)\ncall: pytest(contract_tests)\ncall: smoke(gateway -> graphql)\nassert: no runtime warnings\nverify: dashboard shows p95 below target\nreturn: ready_for_review" ~small !mono >observability_layer +@ box ci "CI Pipeline\nGitHub Actions" 801,1138 200x85 %muted +@ box tests "Integration Tests\ncontract + flow" 60,1131 210x85 %muted >observability_layer +@ box monitor "Prometheus + Grafana\nruntime signals" 368,1195 200x85 %muted >observability_layer +@ note n_verify_code 46,1242 "code:\nfn: verifyChange(pr)\ncall: pytest(contract_tests)\ncall: smoke(gateway -> graphql)\nassert: no runtime warnings\nverify: dashboard shows p95 below target\nreturn: ready_for_review" ~small !mono >observability_layer # --- Frontend to API --- @ arrow web -> gateway "call: HTTPS /api" @@ -93,20 +93,20 @@ Block text note: Use triple quotes when the text itself contains "quoted strings". The canvas still edits this as normal note text. """ ~small -@ note n_subgraph_hint 913.8237143874437,1133.0152145654704 "Sub-grafli demo: open the linked Order Processing box to inspect the detailed order flow." ~small +@ note n_subgraph_hint 914,1133 "Sub-grafli demo: open the linked Order Processing box to inspect the detailed order flow." ~small @ arrow n_orders_code -> auth -@ box box1 "test" -1500,-160 495.03133382484134x516.9951555758935 +@ box box1 "test" -1500,-160 495x517 @ box box2 "3" -1220,200 160x80 %subtle >box1 -@ box box3 "1" -1464.0125969060239,15.924418563856193 160x80 %subtle >box1 -@ box box4 "2" -1436.0503876240962,175.92441856385616 160x80 %subtle >box1 -@ box box5 "5" -1220.06298453012,1.2115632417683173 160x80 %subtle >box1 +@ box box3 "1" -1464,16 160x80 %subtle >box1 +@ box box4 "2" -1436,176 160x80 %subtle >box1 +@ box box5 "5" -1220,1 160x80 %subtle >box1 @ box box6 "4" -1320,-120 160x80 %subtle >box1 @ arrow box3 -> box6 @ arrow box6 -> box5 @ arrow box5 -> box2 @ arrow box5 -> box4 -@ image img1 "architecture-res/img-20260501-190455.png" -1280.852069082695,632.8103352658841 320x120.06279434850863 -@ note n1 -833.2826241509773,552.4008407292996 """ +@ image img1 "architecture-res/img-20260501-190455.png" -1281,633 320x120 +@ note n1 -833,552 """ code: fn: placeOrder(req) -> OrderId pre: req.user is authenticated @@ -119,4 +119,4 @@ post: audit trail includes "order.created" verify: test_order_happy_path @tests/orders_test.py:41 risk: reservation leak if payment timeout is not compensated """ ~small !mono -@ box box7 "3" -791.5671846682503,175.34270062328628 160x80 %subtle +@ box box7 "3" -792,175 160x80 %subtle diff --git a/examples/oauth-callback.grafli b/examples/oauth-callback.grafli index 292ad12..010adab 100644 --- a/examples/oauth-callback.grafli +++ b/examples/oauth-callback.grafli @@ -20,7 +20,7 @@ @ box provider "OAuth Provider\nGoogle / GitHub" 460,140 220x80 %muted # ── Service boxes (horizontal flow) ──────────────── -@ box cb "Callback Handler\nPOST /auth/callback" 37.62564878892732,297.6458693771626 220x80 %primary +@ box cb "Callback Handler\nPOST /auth/callback" 38,298 220x80 %primary @ box xchg "Token Exchange\noauth client" 460,300 220x80 %tertiary @ box users "User Upsert\nidentity store" 840,300 220x80 %tertiary @ box sess "Session Mint\ncookie + redis" 1220,300 220x80 %tertiary @@ -35,7 +35,7 @@ # ── Code notes — one function per service ────────── -@ note n_cb -25.925767733563987,411.76054282006925 """ +@ note n_cb -26,412 """ code: handle_callback(req) -> Response pre req.method == POST @@ -47,7 +47,7 @@ risk timing leak — use constant_time_eq return exchange(req.query.code) @auth/callback.py:23 """ -@ note n_xchg 429.4064121972318,414.1146734429065 """ +@ note n_xchg 429,414 """ code: exchange(code) -> Session token = call provider.post(token_url, code) @@ -57,7 +57,7 @@ profile = call provider.get(userinfo_url, token) return upsert_user(profile) @auth/oauth.py:88 """ -@ note n_users 836.4789143598614,415.2917387543254 """ +@ note n_users 836,415 """ code: upsert_user(profile) -> User existing = find_by_provider_id(profile.id) @@ -69,7 +69,7 @@ emit UserCreated(new.id) return new @users/store.py:14 """ -@ note n_sess 1196.4688040657438,417.6458693771626 """ +@ note n_sess 1196,418 """ code: mint_session(user) -> SessionId sid = random_token(32) @@ -90,7 +90,7 @@ return 302 redirect_to=return_to @auth/callback.py:78 """ # ── Cross-cutting concerns: security tests ───────── -@ box tests "Security Tests\ncontract suite" 892.9477184256054,825.9358780276815 220x80 %muted +@ box tests "Security Tests\ncontract suite" 893,826 220x80 %muted @ arrow tests -> cb "verify: csrf" !dashed @ arrow tests -> sess "verify: fixation" !dashed diff --git a/examples/showcase.grafli b/examples/showcase.grafli index e1f4cdf..00141f7 100644 --- a/examples/showcase.grafli +++ b/examples/showcase.grafli @@ -17,16 +17,16 @@ # Region 1 — Baker's day (hero) # ============================================================ -@ note title1 1476.537054409133,19.010918447672285 "Baker's day" ~xxlarge +@ note title1 1477,19 "Baker's day" ~xxlarge -@ note caption1 -300.72953746032147,-108.89470384589161 "A daily routine as a state machine.\nEach arrow's `step:` / `state:` / `event:` prefix renders as a colored chip on the edge." ~small +@ note caption1 -301,-109 "A daily routine as a state machine.\nEach arrow's `step:` / `state:` / `event:` prefix renders as a colored chip on the edge." ~small -@ box sleep "Sleep" 140,142.19573710323735 160x90 %subtle -@ box bake "Bake bread" 126.08540922305644,389.9602261378962 180x90 %accent -@ box shop "Open shop" 479.1980244720821,505.5838972527878 180x90 %tertiary -@ box lunch "Lunch" 874.0607783666509,391.19624798664864 160x90 %highlight -@ box deliver "Deliver" 846.494085510205,146.77996554352328 200x90 %tertiary -@ box dinner "Dinner" 481.5442312766063,28.774266762025334 160x90 %accent +@ box sleep "Sleep" 140,142 160x90 %subtle +@ box bake "Bake bread" 126,390 180x90 %accent +@ box shop "Open shop" 479,506 180x90 %tertiary +@ box lunch "Lunch" 874,391 160x90 %highlight +@ box deliver "Deliver" 846,147 200x90 %tertiary +@ box dinner "Dinner" 482,29 160x90 %accent @ arrow sleep -> bake "step: 06:00" @ arrow bake -> shop "state: ready" @@ -35,7 +35,7 @@ @ arrow deliver -> dinner "step: 20:00" @ arrow dinner -> sleep "step: 22:00" -@ note bake_code 433.6683666702927,217.99831588774094 "code:\nbake() -> Bread\nif flour < 1\n err OutOfFlour\ndough = mix(flour, water, yeast)\nstate rising -> baking\nemit BatchReady\nreturn bread" +@ note bake_code 434,218 "code:\nbake() -> Bread\nif flour < 1\n err OutOfFlour\ndough = mix(flour, water, yeast)\nstate rising -> baking\nemit BatchReady\nreturn bread" @ arrow bake_code -- bake @ note qa_flour 1400,380 "Q: what if flour runs out mid-bake?\nA: the `code:` note bails — `if: flour < 1` raises OutOfFlour and ends the routine for the day." ~width=61 @@ -44,16 +44,16 @@ # Region 2 — Threat reaction (annotations) # ============================================================ -@ note title2 -38.010045408300755,719.0583777077526 "Threat reaction" ~xxlarge +@ note title2 -38,719 "Threat reaction" ~xxlarge -@ note caption2 -43.35091625087598,781.8723275865401 "Behavior under review. `T:` tasks, `Q:` questions, threaded discussions, and `code:` notes live next to the boxes they describe." ~small +@ note caption2 -43,782 "Behavior under review. `T:` tasks, `Q:` questions, threaded discussions, and `code:` notes live next to the boxes they describe." ~small -@ box sense "Sense threat" -15.943060517962408,1006.7008582809965 220x90 %subtle +@ box sense "Sense threat" -16,1007 220x90 %subtle @ box assess "Assess danger" 340,1000 240x90 %accent &showcase-res/assess.grafli -@ box flee "Flee" 878.2093078937904,1001.3971219715272 180x90 %muted -@ box fight "Fight" 894.4274649918939,1204.877538225958 180x90 %accent -@ box guards "Call guards" 425.3673853221261,1213.1965668760133 200x90 %tertiary -@ box hide "Hide" 874.2108017465225,819.0768264130897 180x90 %muted &showcase-res/hide.md +@ box flee "Flee" 878,1001 180x90 %muted +@ box fight "Fight" 894,1205 180x90 %accent +@ box guards "Call guards" 425,1213 200x90 %tertiary +@ box hide "Hide" 874,819 180x90 %muted &showcase-res/hide.md @ arrow sense -> assess "call: classify" @ arrow assess -> flee "event: weak" @@ -64,10 +64,10 @@ @ arrow fight -> flee "risk: outnumbered" !dashed @ arrow hide -> assess "verify: clear" !dashed -@ note todo -5.769576575522848,1340.638539578403 "T: add audible cue when sense triggers" +@ note todo -6,1341 "T: add audible cue when sense triggers" @ note q1 340,1340 "Q: should fear scale with NPC level?" -@ note disc 683.8763271382929,1345.2969292077053 "Designer: Should child NPCs ever pick fight?\nReviewer: No — clamp to flee/hide when AgeTag is child.\nDesigner: Will add the tag check inside assess." ~width=54 -@ note qa_recover 410.20212342126865,840.4205841046205 "Q: should Hide auto-recover after a timer?\nA: no — only on `verify: clear` from assess. \nA timer risks re-engaging an active threat." ~width=47 +@ note disc 684,1345 "Designer: Should child NPCs ever pick fight?\nReviewer: No — clamp to flee/hide when AgeTag is child.\nDesigner: Will add the tag check inside assess." ~width=54 +@ note qa_recover 410,840 "Q: should Hide auto-recover after a timer?\nA: no — only on `verify: clear` from assess. \nA timer risks re-engaging an active threat." ~width=47 @ note assess_code 20,1140 "code:\nassessDanger(npc, threat) -> Action\nratio = threat.power / npc.power\nif ratio > 2.0\n return flee\nif threat.count > npc.allies\n return guards\nverify tests/test_assess.py:matrix\nreturn fight @ai/threat.py:88" ~width=38 @ arrow assess_code -- assess @@ -77,30 +77,30 @@ @ note title3 -440,1640 "Town life" ~xxlarge ~width=10 -@ note caption3 -435.1967712668878,1804.434821320418 "NPCs `owns:` a shift,\n`step:` through routines,\nreact to world `event:`s.\nPress A for the heatmap." ~small ~width=39 +@ note caption3 -435,1804 "NPCs `owns:` a shift,\n`step:` through routines,\nreact to world `event:`s.\nPress A for the heatmap." ~small ~width=39 # NPCs (column 1) -@ box baker "Baker" -31.225890102333537,1830.8493366059504 180x80 %accent -@ box smith "Smith" -20.7041979645699,2000.605628319995 180x80 %accent -@ box innkeep "Innkeeper" 43.49729606155151,2224.013491138159 180x80 %accent -@ box guard "Guard" 41.20404947985378,2346.9226694018726 180x80 %accent -@ box priest "Priest" 39.09406489545282,2595.5938856643634 180x80 %accent +@ box baker "Baker" -31,1831 180x80 %accent +@ box smith "Smith" -21,2001 180x80 %accent +@ box innkeep "Innkeeper" 43,2224 180x80 %accent +@ box guard "Guard" 41,2347 180x80 %accent +@ box priest "Priest" 39,2596 180x80 %accent # Workplaces (column 2) -@ box bakery "Bakery" 289.19600001982565,1847.071447383281 160.854356394798x62.76892075531828 %tertiary -@ box smithy "Smithy" 169.56183325412547,2126.6826719758637 180x80 %tertiary -@ box inn "Inn" -259.275277523341,2183.4636336263507 180x80 %tertiary -@ box gate "Town gate" 43.616533258049714,2475.10969764214 180x80 %tertiary +@ box bakery "Bakery" 289,1847 161x63 %tertiary +@ box smithy "Smithy" 170,2127 180x80 %tertiary +@ box inn "Inn" -259,2183 180x80 %tertiary +@ box gate "Town gate" 44,2475 180x80 %tertiary @ box temple "Temple" -400,2600 180x80 %tertiary # Shared spaces (column 3) -@ box market "Market" -455.0965933225317,1916.1598782874626 180x80 %highlight -@ box square "Town square" -480.42581859675676,2386.1247142033103 180x80 %highlight -@ box well "Village well" 502.3714955819046,2583.084750790608 180x80 %highlight +@ box market "Market" -455,1916 180x80 %highlight +@ box square "Town square" -480,2386 180x80 %highlight +@ box well "Village well" 502,2583 180x80 %highlight # Events (column 4) -@ box festival "Festival" 652.4330966119542,2331.586373897622 180x80 %primary -@ box raid "Bandit raid" -479.02697596886605,2220.197687009605 180x80 %primary +@ box festival "Festival" 652,2332 180x80 %primary +@ box raid "Bandit raid" -479,2220 180x80 %primary @ box trade "Trade day" 580,2020 180x80 %primary # Each NPC owns a daily shift at their workplace diff --git a/examples/skill-explained.grafli b/examples/skill-explained.grafli index 30a7a5d..04caa2b 100644 --- a/examples/skill-explained.grafli +++ b/examples/skill-explained.grafli @@ -6,16 +6,16 @@ @ note title 350,-80 "How the grafli skill works" ~xxlarge # === Pipeline (left to right) === -@ box prompt "User prompt" 20,80 220x80 %secondary -@ box trigger "Skill triggers?" 280,80 220x80 %accent -@ box plan "Plan" 540,80 220x80 %accent -@ box author "Author + verify" 800,80 220x80 %accent -@ box output "Clean .grafli" 1060,80 220x80 %primary +@ box prompt "User prompt" 20,80 220x80 %secondary +@ box trigger "Skill triggers?" 280,80 220x80 %accent +@ box plan "Plan" 540,80 220x80 %accent +@ box author "Author + verify" 800,80 220x80 %accent +@ box output "Clean .grafli" 1060,80 220x80 %primary -@ arrow prompt -> trigger -@ arrow trigger -> plan "yes" -@ arrow plan -> author -@ arrow author -> output +@ arrow prompt -> trigger +@ arrow trigger -> plan "yes" +@ arrow plan -> author +@ arrow author -> output # === Notes describing each step === @ note prompt_text 20,200 """