From 2e51350ca1276b235cfd3882600a6f3e4e16c07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BD=A9=E8=BE=95=E5=8D=81=E5=9B=9B?= Date: Tue, 1 Sep 2026 20:09:18 +0800 Subject: [PATCH 1/2] fix: support legacy Android foreground detection --- .../AndroidDeviceScreenStateReader.swift | 7 +++--- .../AndroidScreenshotProviderTests.swift | 25 ++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Sources/AstrolabeAndroidScreenshot/AndroidDeviceScreenStateReader.swift b/Sources/AstrolabeAndroidScreenshot/AndroidDeviceScreenStateReader.swift index fb66257..b3f7e38 100644 --- a/Sources/AstrolabeAndroidScreenshot/AndroidDeviceScreenStateReader.swift +++ b/Sources/AstrolabeAndroidScreenshot/AndroidDeviceScreenStateReader.swift @@ -49,9 +49,10 @@ struct AndroidDeviceScreenStateReader: AndroidDeviceScreenStateReading { } private func resumedApplicationIdentifier(from output: String) -> String? { - guard let line = output - .split(whereSeparator: \Character.isNewline) - .first(where: { $0.contains("topResumedActivity=") }) else { + let lines = output.split(whereSeparator: \Character.isNewline) + let line = lines.first(where: { $0.contains("topResumedActivity=") }) + ?? lines.first(where: { $0.contains("mResumedActivity:") }) + guard let line else { return nil } return line diff --git a/Tests/AstrolabeAndroidScreenshotTests/AndroidScreenshotProviderTests.swift b/Tests/AstrolabeAndroidScreenshotTests/AndroidScreenshotProviderTests.swift index 0049f76..ff61636 100644 --- a/Tests/AstrolabeAndroidScreenshotTests/AndroidScreenshotProviderTests.swift +++ b/Tests/AstrolabeAndroidScreenshotTests/AndroidScreenshotProviderTests.swift @@ -59,6 +59,26 @@ final class AndroidScreenshotProviderTests: XCTestCase { ]) } + func testScreenStateReaderResolvesLegacyAndroidResumedActivity() throws { + let runner = ScreenshotADBCommandRunner(results: [ + commandOutput( + "mResumedActivity: ActivityRecord{1553f2c u0 com.example.demo/.MainActivity t31}\n" + ), + commandOutput("mShowingLockscreen=false\n") + ]) + let reader = AndroidDeviceScreenStateReader( + adbClient: ADBClient(commandRunner: runner) + ) + + let state = try reader.read(deviceSerial: "emulator-5554") + + XCTAssertEqual( + state.foregroundApplicationIdentifier, + "com.example.demo" + ) + XCTAssertFalse(state.locked) + } + func testProviderRejectsTargetIdentifierFromAnotherDevice() throws { let provider = AndroidSystemScreenshotProvider( adbClient: ADBClient(commandRunner: ScreenshotADBCommandRunner(results: [])) @@ -151,7 +171,10 @@ final class AndroidScreenshotProviderTests: XCTestCase { func testProviderRejectsCaptureWhenFocusedAppCannotBeResolved() throws { let runner = ScreenshotADBCommandRunner(results: [ - commandOutput("topResumedActivity=null\n"), + commandOutput(""" + topResumedActivity=null + mResumedActivity: ActivityRecord{42 u0 com.example.demo/.MainActivity t1} + """), commandOutput("isStatusBarKeyguard=false\n") ]) let provider = AndroidSystemScreenshotProvider( From 363cbde3f2b75c6d4162844c03edc598757d51ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BD=A9=E8=BE=95=E5=8D=81=E5=9B=9B?= Date: Wed, 2 Sep 2026 00:38:57 +0800 Subject: [PATCH 2/2] docs: simplify Astrolabe skill guidance --- skills/astrolabe/SKILL.md | 273 +++++------------- skills/astrolabe/agents/openai.yaml | 6 +- skills/astrolabe/evals/evals.json | 151 ++-------- .../references/design-verification.md | 167 ----------- .../astrolabe/references/rendered-content.md | 89 ------ .../astrolabe/references/temporary-patches.md | 34 --- skills/astrolabe/references/ui-graph.md | 80 ----- .../astrolabe/references/visual-regression.md | 39 --- test/review-contract.test.mjs | 245 ++++------------ 9 files changed, 140 insertions(+), 944 deletions(-) delete mode 100644 skills/astrolabe/references/design-verification.md delete mode 100644 skills/astrolabe/references/rendered-content.md delete mode 100644 skills/astrolabe/references/temporary-patches.md delete mode 100644 skills/astrolabe/references/ui-graph.md delete mode 100644 skills/astrolabe/references/visual-regression.md diff --git a/skills/astrolabe/SKILL.md b/skills/astrolabe/SKILL.md index 648d07d..212966a 100644 --- a/skills/astrolabe/SKILL.md +++ b/skills/astrolabe/SKILL.md @@ -1,212 +1,69 @@ --- name: astrolabe -description: Use when implementing, debugging, reviewing, or verifying a running mobile UI, including layout, rendered content, visibility, styles, images, text, screenshots, visual diffs, baselines, temporary presentation experiments, and UI relations such as View, Layer, mask, or ownership paths. Prefer Astrolabe runtime evidence over source-only conclusions whenever an inspectable App is available. +description: Use when inspecting a running mobile app UI or reviewing whether an implementation matches supplied design references. --- -# Astrolabe UI Inspector - -Use Astrolabe to observe and verify the UI that is actually running on a -supported mobile platform. Source code describes intent; Runtime nodes, -semantic attributes, relations, and system screenshots show the result. Do not -claim that a UI is correct from source inspection alone when an inspectable App -is running. - -## Core Workflow - -1. Discover the target with `list_apps`. - - Read `diagnostics` before concluding that no App is available. - - Match the user-provided App, application identifier, virtual device, or - physical device. Ask when multiple candidates remain. - - Check `compatibility.status`, `capabilities`, - `missingRuntimeCapabilities`, and `recoverySuggestion` before selecting an - operation. - - Read `platform`; never infer a framework or transport from `appId`, device - names, classes, or attribute namespaces. - - Confirm that the required MCP Tool exists as well as the corresponding - Runtime capability. Tool availability belongs to the installed Host/MCP; - capabilities belong to the selected Runtime. - - Use the exact latest `appId` for every subsequent call. - -2. Establish Target State Readiness before formal acceptance. - - Observe the actual current screen. Do not infer the target from source, - route names, or intended navigation. - - If the target is absent, use interaction only when both its MCP Tool and - Runtime capability exist, and only through visible normal UI. - - Otherwise request developer manual navigation, real-condition preparation, - or explicit debug-fixture/mock authorization. Never use an App Router, - private initializer, unapproved deep link, source shortcut, or business-state - mutation to prepare the target. - - After any preparation, rediscover the App and re-observe the target. Treat - every readiness-observation snapshot as preliminary and discard it; do not - reuse it for formal acceptance. Before readiness, do not issue a page - `passed`, `failed`, or `inconclusive` verdict because acceptance has not - started. - - An authorized mock is presentation-only; end-to-end behavior remains - unverified. Read [design-verification.md](references/design-verification.md) - before any design verification. - -3. Freeze the screen context. - - After readiness, start with `inspect_screen` to create and retain the fresh, - authoritative formal acceptance `snapshotId`. - - For design acceptance, create authoritative Design Expectations and a - complete Coverage Ledger before selecting or running checks. - - Pass that same `appId` and `snapshotId` to hierarchy, lookup, detail, and - structured-check tools while investigating the same state. - - When tree structure or an implementation node is needed, call - `capture_hierarchy` with the existing `snapshotId`; do not silently create - a second snapshot. - - The snapshot freezes hierarchy identity, not necessarily every node detail. - Read `detailSource`: `snapshotCache` is cached evidence, while `liveRuntime` - was resolved later through the frozen node's `detailOid`. A live detail and - its hierarchy are not an atomic observation or guaranteed to have the same - capture time. Compare `detailCapturedAtUnixTime` with `capturedAtUnixTime` - when timing can affect the conclusion. - - Omit `snapshotId` only when the latest state is required or the user has - intentionally navigated. Restart the investigation from the new snapshot. - - A screenshot is always latest-state evidence and is not pixel-synchronized - with an older hierarchy snapshot. - -4. Locate the actual rendering node. - - Use `summarize_hierarchy` when counts and bounded lists are sufficient. Use - `capture_hierarchy` only when the tree shape or implementation nodes matter. - - `capture_hierarchy` returns at most 25 nodes by default. Set `nodeLimit` and - `maxDepth` deliberately, then read `nodeCount`, `returnedNodeCount`, - `omittedNodeCount`, and `truncated`. Do not conclude that a node is absent or - missing from a truncated projection. - - Use `find_nodes` to expose ambiguity before a first-match tool. - - Search broad to narrow: visible text, semantic role, custom class, frame - region, then hierarchy path. - - Inspect nested label, image, Layer, or other content-bearing nodes instead - of stopping at a control or layout container. - - Follow `nextCursor` unchanged while `hasMore` is true. Keep the same - selectors and frozen snapshot; restart when the pagination snapshot - expires or changes. - - Use the selected node's exact `oid` or `detailOid` for follow-up calls. - -5. Prove rendered output, not only layout intent. - - A layout box describes where a container participates in layout; it does - not necessarily describe the rendered footprint of its content. - - Before approving size, containment, clipping, alignment, or visual balance, - inspect the relevant intrinsic content, mapping policy, clipping or masking, - and transform or visual effect evidence. - - For images, read `imageSize`, `imageScale`, `contentMode`, frame, bounds, - and clipping together. A correct frame or bounds alone cannot prove that - the image rendered at the intended size. A larger image under an - unscaled content mode can overflow or crop. - - Missing evidence for a dimension required by the review cannot produce - `passed`; report the result as `inconclusive` and name the missing fact. - - Read [rendered-content.md](references/rendered-content.md) whenever content - can render differently from its container, including images, text, masks, - transforms, shadows, and descendant drawing. - -6. Choose hierarchy or UI Graph deliberately. - - Use hierarchy tools for ordinary parent-child structure. - - Use `query_ui_graph` for ownership, cross-tree, non-hierarchy relations, or - a bounded uniform relation traversal. Require both the Tool and the - `uiGraphRelations` capability. - - The frozen chain is `capture_hierarchy` → `query_ui_graph` → - `summarize_node_detail`, always with the same `appId` and `snapshotId`. - - Request only the necessary namespaced relation types and direction. Read - `truncated`, `truncationReasons`, `frontierOids`, and - `omittedFrontierCount` before expanding. - - Read [ui-graph.md](references/ui-graph.md) for relation semantics, - platform examples, and exact recovery behavior. - -7. Turn requirements into evidence-backed checks. - - Use `check_node` for identity, text, visibility, or exact frame. - - Use `check_node_detail` for one semantic attribute, `check_style` for a - related style group, and `check_layout` for relations between nodes. - - Use `summarize_node_detail` with a narrow filter before raw `node_detail`. - - Structured checks prove exact Runtime facts. Screenshots prove visual - composition. Use both when the conclusion depends on both. - - Read [visual-regression.md](references/visual-regression.md) for screenshot, - diff, baseline, coordinate, and dynamic-region rules. - -8. Iterate until source-backed behavior is verified. - - After source changes, build and relaunch only when permitted by the user and - repository instructions. - - Run `list_apps` again after relaunch because `appId` can change. Capture a - fresh hierarchy because node identifiers are process-local. - - Temporary patches test hypotheses; they are not implementation. Read - [temporary-patches.md](references/temporary-patches.md) before patching and - restore all patches before final verification. - -## Tool Routing - -| Goal | Preferred tool | Gate | -| --- | --- | --- | -| Discover Apps and compatibility | `list_apps` | Always first and after relaunch | -| Understand the current screen | `inspect_screen` | Retain `snapshotId` | -| Summarize hierarchy contents | `summarize_hierarchy` | Prefer when tree shape is unnecessary | -| Inspect bounded tree structure | `capture_hierarchy` | Reuse the snapshot and inspect truncation metadata | -| Find candidate nodes | `find_nodes` | Page without changing selectors or snapshot | -| Find and inspect one unique node | `inspect_node` | Establish uniqueness first | -| Read semantic properties | `summarize_node_detail` | Use raw `node_detail` only for omitted structure | -| Trace UI relations | `query_ui_graph` | Require Tool plus `uiGraphRelations` | -| Assert one node or property | `check_node`, `check_node_detail` | Use exact expectations and tolerances | -| Assert grouped style or layout | `check_style`, `check_layout` | Inspect the real content-bearing nodes | -| Capture latest visual state | `capture_screenshot` | Do not treat it as frozen-snapshot evidence | -| Compare or explain pixels | `compare_screenshot`, `inspect_diff` | Keep source, scale, and viewport explicit | -| Record or compare a reference | `record_baseline`, `compare_baseline` | Record only an accepted stable state | -| Test a presentation hypothesis | patch lifecycle tools | Read the patch reference and restore state | - -## Evidence Gates - -- Container geometry is insufficient when content can scale, align, overflow, - crop, wrap, transform, cast effects, use masks, or draw descendants. -- A screenshot alone is insufficient for exact logical values. Runtime - attributes alone are insufficient for final visual composition. -- Runtime geometry uses logical units. Screenshot dimensions and ignore regions - use pixels. Convert only with the reported logical-to-pixel scale. -- A shared `snapshotId` keeps node identity and hierarchy stable, but a - `liveRuntime` detail can be captured later. Treat timing-sensitive mixed - evidence as `inconclusive` unless the time gap is irrelevant or reverified. -- A truncated hierarchy can prove facts about returned nodes, but cannot prove - that an unreturned node or relationship is absent. -- Treat `visible` and `onscreen` as screen intersection. Use - `hierarchyVisible` to distinguish otherwise displayable offscreen or - ancestor-clipped nodes. -- Do not prescribe a universal scaling or clipping mode. Compare actual - rendering semantics with the product requirement. - -## Recovery - -- No App: report discovery diagnostics and their recovery suggestion. -- Incompatible Runtime: report platform, Host/Runtime versions, `errorCode`, - missing capabilities, and `recoverySuggestion`. -- Required Tool absent: report a Host/MCP installation or version gap; do not - misreport it as a missing Runtime capability. -- Stale App or node: rediscover the App and capture a new snapshot. -- Expired or changed snapshot: restart the complete workflow; never combine old - and new nodes, details, cursors, or relations. -- Ambiguous node: report candidates and narrow the selector before asserting. -- Missing detail: report the unavailable fact. Do not substitute a current node - under an old `snapshotId`. -- Screenshot failure: report source, target identifier, and recovery suggestion; - do not fall back to an unrelated target. - -## Reporting - -Report only evidence relevant to the question: - -- Target App, platform, device, `appId`, and required capabilities. -- `snapshotId`, hierarchy source, and capture time for frozen evidence. When - details are used, also report `detailSource` and `detailCapturedAtUnixTime`. -- Nodes inspected: class, text when relevant, `oid`, frame, and content-bearing - role. -- Exact attributes or relation paths, expected and actual values, tolerances, - and any missing evidence. -- Screenshot source, dimensions, scale, and diff result when visual evidence is - used. -- Active temporary patches, clearly labeled as experiments. -- For design verification: readiness, preparation mode, authorization, and - verification scope; design provenance and reference viewport; Target Context; - Coverage Ledger counts; each failed or inconclusive expectation with expected, - actual, and tolerance or range; screenshot/structured-evidence consistency; - and final verdict. - -For formal acceptance after readiness, end with `passed`, `failed`, or -`inconclusive`. Only source implementation plus clean Runtime verification can -produce `passed`. Before readiness, report only readiness, blocking condition, -and requested preparation. For exploratory or factual questions, answer the -finding directly and state uncertainty instead of forcing a pass/fail label. +# Astrolabe + +Use Astrolabe MCP to read the UI hierarchy, node properties, and screenshots of a mobile app that is currently running. UI review is read-only by default, and every conclusion must be based on current runtime evidence. + +## 1. Acceptance prerequisites + +First use `list_apps` to check runtime availability and compatibility. Only then use `inspect_screen` to confirm the target state. Before formal acceptance, all of these conditions must hold: + +- The app must be running and remain in the foreground. +- `list_apps` finds the target app, and its `compatibility.status` allows inspection to continue. +- `inspect_screen` confirms that the current screen shows the UI, component, or state to be reviewed. + +A successful build, passed unit tests, completed installation, or an app that was launched and then killed does not make the app inspectable. + +If `list_apps` cannot find the target app, stop and ask the developer to launch it and keep it in the foreground. If compatibility does not allow inspection, report `compatibility.status` and `recoverySuggestion`, then stop. If the target state is not shown, state which UI, component, or state the developer must prepare, then wait for the developer. + +Keep acceptance read-only: do not click, scroll, type, trigger UI elements, events, routes or deep links, or change business state. + +After the developer prepares the target state, restart from `list_apps`. + +## 2. Basic Astrolabe MCP calls + +Call the tools in this order: + +1. `list_apps`: confirm that the target app is discoverable and obtain the latest `appId`; check `compatibility.status`, and report `recoverySuggestion` and stop if incompatible. +2. `inspect_screen`: confirm that the current UI and state match the acceptance target, then obtain a new `snapshotId`. +3. `find_nodes`: find candidate nodes by text, semantic role, or class name. +4. `inspect_node` or `summarize_node_detail`: read a node's frame, text, style, and other properties. +5. `capture_screenshot`: capture the current rendered result. +6. For exact assertions, use `check_node`, `check_node_detail`, `check_style`, or `check_layout`. + +Within one static inspection, reuse the same `appId` and `snapshotId` for hierarchy, lookup, detail, and assertion tools. A screenshot represents the latest state at capture time and is not guaranteed to be synchronized with an older snapshot. + +After the app is restarted, killed, or reinstalled, run `list_apps` and `inspect_screen` again and discard the old `appId`, `snapshotId`, and node identifiers. If only the target state changes, run `list_apps` again to confirm the current `appId`, then run `inspect_screen` for a new `snapshotId`; the `appId` may stay the same, but do not reuse the old snapshot or node evidence. + +If multiple nodes match, narrow the selector instead of guessing. When evidence is missing, report “unable to verify” instead of inferring runtime behavior from source code. + +## 3. Design consistency review + +Confirm the design references, target UI state, and runtime environment before inspecting the current screen. Design references may include design tool files, images, annotations, visual specifications, design systems, or an accepted reference UI. If the runtime state does not match the acceptance target, follow the prerequisites and wait for the developer. + +Compare each design requirement with the runtime actual: + +- Text, font family, font size, font weight, line height, and wrapping +- Text, background, border, and icon colors and opacity +- Position, width, height, alignment, relative spacing, and safe areas +- Corner radius, borders, shadows, clipping, and occlusion +- Image or icon assets, aspect ratio, scaling, and rendered output + +Use node properties for exact values and screenshots for overall composition. Verify relative spacing from the relevant node frames or with `check_layout`. + +Use this report structure: + +```markdown +### Target +- App, runtime environment, target state, `appId`, `snapshotId`, and node `oid` + +### Differences +| Item | Design requirement | Runtime actual | Evidence | Result | +| --- | --- | --- | --- | --- | + +### Conclusion +- Aligned / Not aligned / Unable to verify +``` diff --git a/skills/astrolabe/agents/openai.yaml b/skills/astrolabe/agents/openai.yaml index 19622ef..5a78dd8 100644 --- a/skills/astrolabe/agents/openai.yaml +++ b/skills/astrolabe/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "Astrolabe UI Inspector" - short_description: "Verify a running mobile app UI against design contracts and Runtime evidence" - default_prompt: "Use Astrolabe MCP to observe the real target state, prepare it only through authorized visible UI or developer assistance, capture a fresh acceptance snapshot, and verify design contracts, rendered content, styles, UI relations, and screenshots with capability-driven evidence." + display_name: "Astrolabe UI Inspection" + short_description: "Inspect running mobile app UIs and review design consistency" + default_prompt: "Use $astrolabe to inspect a currently running app, wait for the developer to prepare the target state, collect node properties and screenshots read-only, and report differences from the supplied design references." diff --git a/skills/astrolabe/evals/evals.json b/skills/astrolabe/evals/evals.json index 2c20462..1d27957 100644 --- a/skills/astrolabe/evals/evals.json +++ b/skills/astrolabe/evals/evals.json @@ -1,136 +1,27 @@ { + "skill_name": "astrolabe", "evals": [ { - "id": "fixed-spacing-failure", - "prompt": "Review the ProfileHeader on Android. Design source: PRD DS-42 requires the avatar-to-name gap to be exact 10 logical units. Target Context: 360x800 compact portrait, fontScale 1.0, English, light appearance. Frozen Runtime evidence: Avatar frame {x: 16, width: 40}; Name frame {x: 74, width: 112}; both nodes are unique and visible. Screenshot has no contradiction. Verify the required spacing.", - "expected_output": "failed; report exact expected 10, actual 18, measurement tolerance, and the unique node evidence.", - "files": ["skills/astrolabe/SKILL.md", "skills/astrolabe/references/design-verification.md"] - }, - { - "id": "contact-cell-scale2-quantization-fixed-gap-regression", - "prompt": "This is a Skill internal rules regression using supplied frozen evidence, not a normal acceptance request that should start or require additional devices. Review the presentation-only ContactCell design fixture for one supplied iPad mini Target Context: 744x1133 at displayScale 2. Scope is only uiGraph.contactCell.section and its four uniquely identified rows: short, long, extreme, and invalidFixedGapRegression. Coverage inventory: geometry 12 required, spatial relations 28 required, final composition 1 required from tail-truncation screenshot evidence. Typography identity, color, glyph rendering, navigation, business conditions, lifecycle, and release behavior are outside scope; do not count adaptive behavior separately from spatial relations. Avatar and CallIcon sizes are 20x20; Avatar leading and CallIcon trailing edges are 15 for every row. Avatar-to-Title exact-10 gaps for short/long/extreme/invalid are 10, 10, 10, and 18; the invalid row source tolerance is 0.01. Title-to-Call minimum-10 gaps for short/long/extreme/invalid are 597, 447, 10, and 405.5. All other required centerY differences are 0; title-to-call same-center-y actual is -0.25, with no source-declared tolerance. Screenshot has no contradiction. Determine the scoped verdict and report raw actuals.", - "expected_output": "failed overall because the required fixed Avatar-to-Title gap is expected 10 and actual 18 beyond tolerance 0.01; retain the raw actual. Pass the legal Title-to-Call minimum gaps. Derive the display-scale-2 quantization budget as 1/2 = 0.5 logical unit and pass title-to-call same-center-y actual -0.25 as measurement quantization, not layout flexibility. Report the supplied-evidence Coverage Ledger as geometry 12/12 passed, spatial relations 27/28 passed with 1 failed, and final composition 1/1 passed, plus the scoped presentation-only range without requiring a normal acceptance run on multiple devices.", - "files": ["skills/astrolabe/SKILL.md", "skills/astrolabe/references/design-verification.md"] - }, - { - "id": "legal-adaptive-spacing", - "prompt": "Review a Settings row on an iPhone SE. Figma shows a 40-unit title-to-toggle gap on a wider reference frame, but its Auto Layout notes declare a minimum gap of 10 and distribute remaining space. Target Context: 320x568 portrait, English, fontScale 1.0. Frozen Runtime frames measure a 18-unit title-to-toggle gap, with fixed 16-unit row edges preserved. Verify this required adaptive spacing.", - "expected_output": "passed for the minimum-gap expectation; do not compare the adaptive gap to the static Figma coordinate.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "missing-adaptive-contract", - "prompt": "Review a tablet dashboard spacer. The only design material is a static Figma frame at 1440x1024 showing a 56-unit gap; it has no Auto Layout, breakpoint, min/max, token, formula, or matching accepted baseline. Target Context is 800x1280 portrait. Runtime measures 32 units. The spacer is required. Determine the verdict.", - "expected_output": "inconclusive; identify the missing authoritative adaptive contract and do not widen tolerance.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "ambiguous-selector", - "prompt": "Review the Checkout screen. The requirement names the visible \"Total\" label, but find_nodes returns two visible UILabel candidates with identical text: a sticky footer and an accessibility mirror. Their frames differ and neither hierarchy path is specified by the contract. Target Context is 390x844 portrait. Verify the label font size.", - "expected_output": "inconclusive; report selector ambiguity and require a unique node before checking.", - "files": ["skills/astrolabe/SKILL.md", "skills/astrolabe/references/design-verification.md"] - }, - { - "id": "image-center-mode-overflow", - "prompt": "Review the Wallet empty-state icon. Design requirement: the icon must be fully contained inside an exact 27x27 logical-unit box. Target Context: 390x844 portrait at displayScale 3. Frozen Runtime evidence: image view frame 27x27, intrinsic image 54x54, contentMode center, imageScale 1, clipsToBounds false, and no mask or transform reduces the rendered footprint. The latest screenshot shows the icon extending beyond its 27x27 box. Verify the required image rendering.", - "expected_output": "failed; the declared containment requirement conflicts with center-mode overflow. Do not treat center mode as universally incorrect.", - "files": ["skills/astrolabe/SKILL.md", "skills/astrolabe/references/design-verification.md"] - }, - { - "id": "target-absent-ios-physical-device", - "prompt": "On an iOS physical device, list_apps and inspect_screen show Home, while the requested SubscriptionOffer screen is absent. The selected app has no interaction Tool and no interaction capability. The source mentions a Router route, but no developer authorization exists for a shortcut. What should happen before design acceptance?", - "expected_output": "do not start acceptance or issue a verdict; request developer manual navigation, real-condition preparation, or authorized debug fixture/mock. Do not use the Router.", - "files": ["skills/astrolabe/SKILL.md", "skills/astrolabe/references/design-verification.md"] - }, - { - "id": "authorized-mock-presentation-only", - "prompt": "A developer explicitly authorizes a debug mock that presents OrderTracking with a delayed shipment card. After rediscovery and formal snapshot capture, verify the card's presentation against the supplied design contract. State the correct verification scope.", - "expected_output": "verify presentation-only scope and state that navigation, business conditions, data mapping, lifecycle, and release behavior remain unverified.", - "files": ["skills/astrolabe/SKILL.md", "skills/astrolabe/references/design-verification.md"] - }, - { - "id": "exact-policy-pass", - "prompt": "Review the Search field in a compact Android phone context: 360x800, fontScale 1.0. Token radius-md requires an exact corner radius of 12 logical units. The unique input node detail reports cornerRadius 12.0 with a 0.1 measurement tolerance. Verify the required exact expectation.", - "expected_output": "passed; actual 12.0 satisfies exact 12 within measurement tolerance.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "exact-policy-fail", - "prompt": "Review the Search field in the same compact Android context. Token radius-md requires an exact corner radius of 12 logical units. The unique input node detail reports cornerRadius 16.0 with a 0.1 measurement tolerance. Verify the required exact expectation.", - "expected_output": "failed; actual 16.0 violates exact 12.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "minimum-policy-pass", - "prompt": "Review an iOS Account row at 390x844. Figma Auto Layout declares that the username-to-chevron gap has a minimum of 12 logical units. Unique frozen frames measure 20 units; fixed row edges and center alignment also hold. Verify the required minimum expectation.", - "expected_output": "passed; actual 20 is not below the declared minimum 12.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "minimum-policy-fail", - "prompt": "Review the same iOS Account row at 320x568 with a long localized username. The contract still requires the username-to-chevron gap to be at least 12 logical units. Unique frozen frames measure 6 units. Verify the required minimum expectation.", - "expected_output": "failed; actual 6 is below the declared minimum 12.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "maximum-policy-pass", - "prompt": "Review the Android tablet notification banner at 1280x800. The PRD limits the banner width to a maximum of 640 logical units. The unique banner frame reports width 600. Verify the required maximum expectation.", - "expected_output": "passed; actual 600 does not exceed maximum 640.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "maximum-policy-fail", - "prompt": "Review the Android tablet notification banner at 1280x800. The PRD limits the banner width to a maximum of 640 logical units. The unique banner frame reports width 712. Verify the required maximum expectation.", - "expected_output": "failed; actual 712 exceeds maximum 640.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "range-policy-pass", - "prompt": "Review the iOS onboarding illustration spacer in regular portrait size class. The design system permits a range of 24 through 48 logical units after safe-area adjustment. Target Context: 834x1194, top safe area 24. Unique frozen frames measure 36. Verify the required range expectation.", - "expected_output": "passed; actual 36 lies within the inclusive range 24 through 48.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "range-policy-fail", - "prompt": "Review the iOS onboarding illustration spacer in regular portrait size class. The design system permits a range of 24 through 48 logical units after safe-area adjustment. Target Context: 834x1194, top safe area 24. Unique frozen frames measure 56. Verify the required range expectation.", - "expected_output": "failed; actual 56 lies outside the inclusive range 24 through 48.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "relation-policy-pass", - "prompt": "Review a MusicPlayer control strip on Android. The design contract requires previous, play, and next icons to share one centerY relation. Target Context: 412x915 portrait. Unique frozen frames produce centerY values 768, 768, and 768. Verify the required relation expectation.", - "expected_output": "passed; all three unique nodes satisfy the declared centerY relation.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "relation-policy-fail", - "prompt": "Review a MusicPlayer control strip on Android. The design contract requires previous, play, and next icons to share one centerY relation. Target Context: 412x915 portrait. Unique frozen frames produce centerY values 768, 768, and 776. Verify the required relation expectation.", - "expected_output": "failed; the next icon violates the declared centerY relation.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "derived-policy-pass", - "prompt": "Review a tablet compose screen. The design contract derives the editor height as viewportHeight 900 minus fixed top bar 64, fixed action bar 56, bottom safe area 20, and two fixed 16-unit vertical gaps: 728 logical units. Target Context is 600x900 with bottom safe area 20. The unique editor frame height is 728. Verify the required derived expectation.", - "expected_output": "passed; actual 728 equals the declared Target Context derivation.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "derived-policy-fail", - "prompt": "Review a tablet compose screen. The design contract derives the editor height as viewportHeight 900 minus fixed top bar 64, fixed action bar 56, bottom safe area 20, and two fixed 16-unit vertical gaps: 728 logical units. Target Context is 600x900 with bottom safe area 20. The unique editor frame height is 692. Verify the required derived expectation.", - "expected_output": "failed; actual 692 does not equal the declared Target Context derivation of 728.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "conditional-policy-pass", - "prompt": "Review the Android navigation rail. The contract is conditional: compact width uses exact 16-unit content edges; expanded width uses exact 24-unit content edges. Target Context is expanded width 840. The unique rail content frame begins 24 units from the edge. Verify the required conditional expectation.", - "expected_output": "passed; expanded-width condition selects the exact 24-unit policy and actual is 24.", - "files": ["skills/astrolabe/references/design-verification.md"] - }, - { - "id": "conditional-policy-fail", - "prompt": "Review the Android navigation rail. The contract is conditional: compact width uses exact 16-unit content edges; expanded width uses exact 24-unit content edges. Target Context is expanded width 840. The unique rail content frame begins 16 units from the edge. Verify the required conditional expectation.", - "expected_output": "failed; expanded-width condition selects 24 but actual is 16.", - "files": ["skills/astrolabe/references/design-verification.md"] + "id": 1, + "prompt": "The Android project's unit tests have passed. The app was launched and then killed, and the developer has just relaunched it to Home; the old appId and snapshotId are still in context. list_apps now finds the app, but compatibility.status is incompatible and includes a recoverySuggestion. Immediately capture the Order Details view, which is not currently shown, and review its UI; you know the Home entry point and deep link.", + "expected_output": "Discard the stale identifiers and obtain the current appId from list_apps. Check compatibility.status, report recoverySuggestion, and stop because the Runtime is incompatible. Do not call inspect_screen, click, scroll, invoke a deep link, or trigger any UI element, event, or route. Only after compatibility is restored and the developer prepares the target state should the workflow restart with list_apps and inspect_screen.", + "files": [], + "expectations": [ + "Does not reuse the appId, snapshotId, or node identifiers from before the app was killed", + "Checks compatibility.status before any other MCP call, reports recoverySuggestion, and stops when incompatible", + "Does not navigate or trigger any UI element, event, route, or deep link" + ] + }, + { + "id": 2, + "prompt": "The target app is in the foreground and currently shows the Profile screen to review. The reviewer provides several design references: the design system specifies an 18 pt Semibold #111111 title; an annotated image requires a 64x64 avatar and 12-point avatar-to-title spacing; the visual specification requires a 16-point card corner radius; and an accepted reference screenshot is also provided. Use Astrolabe MCP to review the implementation against these references.", + "expected_output": "Collect current runtime evidence in this order: list_apps, inspect_screen, find_nodes, node details, and screenshot; use check_node, check_node_detail, check_style, or check_layout where exact assertions are needed. Compare typography, colors, dimensions, relative spacing, corner radius, shadows, and overall composition. Report the target UI and nodes, design requirements, runtime actuals, evidence, and results. Mark missing evidence as unable to verify without guessing or restricting the source or format of design references.", + "files": [], + "expectations": [ + "Treats the design system, annotated image, visual specification, and reference screenshot as one set of design references", + "Reuses one appId and snapshotId for runtime node inspection and identifies the screenshot as latest-state evidence", + "Reports appId, snapshotId, node oid, design requirements, runtime actuals, evidence, and results" + ] } ] } diff --git a/skills/astrolabe/references/design-verification.md b/skills/astrolabe/references/design-verification.md deleted file mode 100644 index f0dfd43..0000000 --- a/skills/astrolabe/references/design-verification.md +++ /dev/null @@ -1,167 +0,0 @@ -# Design Verification - -Use this reference only after Target State Readiness is `ready`. It defines -design intent; Runtime evidence only reports what is rendered. - -## Target State Readiness - -Observe the current screen first. If the target screen or View is absent, use -visible normal UI interaction only when both the Tool and Runtime capability -exist. Otherwise request one developer choice: manual navigation, preparation -of real business conditions, or explicit debug-fixture/mock authorization. - -Never use an App Router, internal route, private initializer, unapproved deep -link, source shortcut, or business-state mutation. After preparation, -rediscover and re-observe the target, then discard every preliminary readiness -snapshot. Only the later formal-acceptance step creates the authoritative -`inspect_screen` snapshot. Before readiness, report the blocking condition and -preparation request, not a page verdict. - -Record `status`, observed and target screen, blocking condition, preparation -mode, developer authorization, and verification scope. An authorized mock is -`presentation-only`: it can verify rendering but leaves navigation, business -conditions, data mapping, lifecycle, and release behavior unverified. - -## Design Source Priority and Target Context - -Use design sources in this priority order: - -1. Explicit PRD requirements, design annotations, and Design Tokens. -2. The matching Figma variant and its Auto Layout, constraints, min/max, - hug/fill, spacing mode, and breakpoint rules. -3. An accepted baseline with the same Target Context. -4. No authoritative source: mark each affected required expectation - `inconclusive`. - -Target Context records the Runtime conditions that determine which contract -applies: `viewportWidth`, `viewportHeight`, `safeAreaInsets`, `orientation`, -`sizeClass`, `displayScale`, `fontScale`, `locale`, `appearance`, and -`systemOccupancy`. Keep logical units distinct from screenshot pixels. - -Default acceptance scope is the current Target Context supplied by the -developer. A passing current device is sufficient for that verdict. Do not -automatically start, switch to, or require another device, simulator, viewport, -or breakpoint because an adaptive or display-scale policy exists. Expand scope -only when the developer explicitly requests multi-device, breakpoint, or -additional Target Context acceptance. - -## Design Expectation Record - -Create one record for every required design fact. Explain every field in the -review artifact: - -| Field | Meaning | -| --- | --- | -| `identifier` | Stable, reportable requirement name. | -| `category` | Content/state, geometry, spacing, typography, color, appearance, image rendering, adaptive behavior, or final composition. | -| `targets` | One or more uniquely located design nodes. | -| `metric` | Measured fact, such as width, spacing, centerY, fontSize, or color. | -| `policy` | Comparison semantics: exact, minimum, maximum, range, relation, derived, or conditional. | -| `expected` | Value, boundary, relation, or formula inputs required by the policy. | -| `unit` | Logical unit, pixel, ratio, color, or another explicit measurement unit. | -| `tolerance` | Measurement or floating-point error only. It is not layout flexibility. | -| `conditions` | Viewport, orientation, size class, locale, or state where this record applies. | -| `required` | Whether this record affects the page verdict. | -| `source` | PRD entry, Figma node, Design Token, or accepted baseline provenance. | - -Do not create expectations from the current implementation. A selector must -identify one unique node before a check can pass. Missing uniqueness, -coordinate-system clarity, authoritative expectation, or required evidence is -`inconclusive`. - -## Policy Semantics - -| Policy | Meaning | -| --- | --- | -| `exact` | Actual equals the fixed expected value within measurement error. | -| `minimum` | Actual is not below the declared lower bound. | -| `maximum` | Actual is not above the declared upper bound. | -| `range` | Actual lies in the declared inclusive interval. | -| `relation` | Nodes satisfy declared alignment, equality, ordering, or spacing relation. | -| `derived` | Expected value is calculated from Target Context and declared fixed inputs. | -| `conditional` | Select a declared sub-policy for the matching breakpoint, orientation, or state. | - -Treat font, size, weight, color, corner radius, border, fixed component size, -fixed edge inset, and fixed internal gap as `exact` unless the source says -otherwise. Treat a gap as flexible only when its source declares a minimum, -maximum, range, remaining-space formula, hug/fill, space-between, safe-area -rule, or breakpoint policy. Tolerance is measurement error only; never use a -large tolerance to imitate a flexible range. - -## Display Quantization Tolerance - -Tolerance covers renderer measurement and display quantization only; it never -adds layout flexibility or changes a fixed source contract. For frame-derived -geometry and relations, derive the default quantization budget from Target -Context `displayScale`: at most `1 / displayScale` logical unit, or one physical -pixel. Use the smallest tolerance that explains the evidence. If a source or -token declares a stricter tolerance, use that stricter value. If display scale -or coordinate provenance is unavailable, do not infer a budget. - -Report raw `actual` values even when they pass within this budget. Apply the -declared tolerance explicitly: `exact` passes when absolute difference is at -most tolerance; `minimum` passes when actual is at least lower bound minus -tolerance; `maximum` passes when actual is at most upper bound plus tolerance; -and `range` passes when actual is within the inclusive bounds expanded by the -tolerance. This budget cannot conceal a meaningful boundary violation, overflow, -or fixed-gap regression. - -For example, at displayScale 2, a measured `centerY` relation difference of -`-0.25` passes within the 0.5 logical-unit quantization budget. An exact -Avatar-to-Title requirement of 10 with actual 18 and tolerance 0.01 still -fails. Fixed component sizes and edge insets remain exact unless their source -declares another policy. - -## Coverage and Evidence Gates - -Maintain a Coverage Ledger with required expectation counts for: content and -state; geometry; spatial relations; typography; color; appearance; image -rendering; adaptive behavior; and final composition. A page has complete -coverage only when every required expectation is passed, failed, inconclusive, -or `notApplicable` with its evidence status recorded. `notApplicable` is valid -only when the authoritative contract says the expectation does not apply to -current conditions. It is neither unchecked nor a result that blocks `passed` -by itself; an Agent must not use it to evade a required check. - -The Coverage Ledger must be complete for the current acceptance scope and -Target Context only. Other viewports are outside the current verdict by default; -their absence neither blocks `passed` nor makes the result `inconclusive`. - -Use frozen Runtime evidence for exact values and relations. Confirm coordinates -use the same coordinate space before calculating a gap or alignment. Use latest -screenshots for composition and correlate them to Runtime nodes. If a screenshot -reveals an unexplained overflow, clipping, occlusion, or other contradiction, -the page cannot pass even when structured checks pass; investigate with a fresh, -consistent evidence chain. - -Aggregate only after readiness: - -```text -any required failed -> failed -otherwise any required inconclusive or unchecked -> inconclusive -otherwise all applicable required expectations passed and no unexplained screenshot contradiction -> passed -``` - -## ContactCell Example - -This example illustrates per-gap policies; it is not a universal component -template. For `ContentView`, `Avatar`, `Title`, and `CallIcon`: - -- Avatar and CallIcon are exactly 20x20. -- Avatar leading and CallIcon trailing edges are exactly 15. -- Avatar-to-Title spacing is exactly 10. -- Title-to-CallIcon spacing is a minimum of 10. -- Avatar, Title, and CallIcon each have a `centerY` relation to ContentView. - -The Title-to-CallIcon gap can grow with remaining space, while the other stated -gaps remain fixed. A static Figma coordinate alone does not authorize that -growth; the flexible contract does. - -## Reporting - -For every design verification, report readiness and preparation, authorization -and scope, design provenance and reference viewport, Target Context, Coverage -Ledger counts, every failed or inconclusive expectation with expected/actual and -tolerance or range, screenshot/structured-evidence consistency, and final -verdict. For `presentation-only`, explicitly list end-to-end facts left -unverified. diff --git a/skills/astrolabe/references/rendered-content.md b/skills/astrolabe/references/rendered-content.md deleted file mode 100644 index bc83722..0000000 --- a/skills/astrolabe/references/rendered-content.md +++ /dev/null @@ -1,89 +0,0 @@ -# Rendered Content Verification - -Read this reference whenever a review concerns apparent size, containment, -cropping, overflow, alignment, or visual balance. The governing rule is: - -> The layout box and the rendered footprint are separate facts. - -A container can have the expected frame while its content renders larger, -smaller, cropped, distorted, offset, transformed, or outside that frame. - -## Five Evidence Layers - -Evaluate only the layers relevant to the target, but do not skip one that can -change the conclusion. - -1. **Layout box**: frame, bounds, constraints, and position in the parent. -2. **Intrinsic content**: image dimensions, text/glyph content, shape path, - descendant drawing, or another natural content extent. -3. **Mapping policy**: scaling, aspect handling, alignment, content gravity, - wrapping, line limits, or drawing mode. -4. **Clipping or masking**: container clipping, ancestor clipping, masks, and - crop regions. -5. **Transform or visual effect**: transforms, shadows, borders, filters, and - other drawing that changes the visible footprint without changing layout. - -If a required evidence layer is unavailable, state which fact is missing. The -container frame alone cannot produce `passed` for a rendered-size or -containment requirement. - -## General Decision Process - -1. Locate the node that actually draws the content. A button or cell may only - contain the image, label, Layer, or custom drawing node that matters. -2. Record the layout box in Runtime logical units. -3. Inspect intrinsic content evidence and the policy that maps it into the box. -4. Determine whether clipping or masking constrains the visible result. -5. Inspect transforms and visual effects when they can extend or move pixels. -6. Use a latest screenshot to confirm composition. Do not claim it is - synchronized with an older hierarchy snapshot. -7. Compare the resulting semantics with the requirement. Intentional overflow, - crop, or effects may be valid, but they require explicit evidence. - -## Images And Icons - -Inspect the image-bearing node, not only its container. Read the returned -platform-namespaced equivalents of: - -- bounds and frame; -- `imageSize` and `imageScale`; -- `contentMode` or platform mapping policy; -- clipping, masks, insets, resizable regions, and transforms when exposed; -- rendering mode when it affects appearance. - -Use these consequences rather than memorizing one platform-specific fix: - -| Evidence | Consequence to verify | -| --- | --- | -| Intrinsic image exceeds bounds under an unscaled mode such as center or edge alignment | The rendered content remains intrinsically sized; it either overflows or is clipped | -| Aspect fit | Content stays inside the box but may leave unused space | -| Aspect fill | Content covers the box but may crop outside its aspect-preserving projection | -| Stretch or scale-to-fill with different aspect ratios | Content may distort | -| Unknown mapping or clipping policy | Rendered containment is unproven | - -Do not prescribe `scaleAspectFit`, clipping, or asset resizing automatically. -First identify the expected behavior. An intentionally oversized illustration -and an icon that must remain inside its control have different contracts. - -## Text And Custom Drawing - -The same model applies beyond images: - -- Text can exceed a label's expected visual region through wrapping, truncation, - line height, font metrics, transforms, or disabled clipping. -- A shape path can draw outside its Layer bounds or be altered by a mask. -- Sublayers and custom drawing can extend outside a View's frame. -- Shadows, borders, and filters can expand the visible footprint without - changing bounds. -- Transforms can change apparent size and position after layout. - -When Runtime details do not expose the decisive drawing fact, use available -relations to inspect the real rendering node and visual evidence to confirm the -result. Missing decisive evidence means `inconclusive`, not `passed`. - -## Approval Gate - -For claims such as “correct size”, “fits inside”, “not cropped”, “aligned”, or -“matches the design”, require evidence for every applicable layer above. Report -the observed mismatch or risk explicitly; do not let a correct container frame -cancel contradictory content evidence. diff --git a/skills/astrolabe/references/temporary-patches.md b/skills/astrolabe/references/temporary-patches.md deleted file mode 100644 index 6948c84..0000000 --- a/skills/astrolabe/references/temporary-patches.md +++ /dev/null @@ -1,34 +0,0 @@ -# Temporary Attribute Patches - -Use patches to test a concrete presentation hypothesis without rebuilding. -Patches are not implementation and cannot prove a source fix. - -## Workflow - -1. Confirm both `attributePatchDiscovery` and `attributePatching`. -2. Call `list_patchable_attributes`. Select an advertised `attributePattern` - compatible with the target role. Empty `targetRoles` means any compatible - node role. -3. Refresh the hierarchy and locate the latest live node. Patch calls target - live Runtime objects rather than frozen snapshot data. -4. For parameterized patterns, use the concrete identifier returned by node - details. -5. Encode the value exactly as required by `valueType`, `valueConstraints`, - `acceptedFormats`, and allowed-value casing. -6. Apply one hypothesis with `apply_attribute_patch` and confirm `actualValue`. -7. Use `list_attribute_patches` to verify active state, then re-read live detail - and capture visual evidence when relevant. -8. If disproved, revert immediately. If confirmed, translate the hypothesis into - a source change. -9. Use `revert_attribute_patch` for one experiment or - `clear_attribute_patches` for all experiments. -10. Rebuild or relaunch, obtain a new `appId`, and verify source-backed behavior - with `patchCount == 0`. - -The Runtime catalog is the only source of truth for supported paths. Never use a -remembered whitelist or infer an unadvertised property. If `patchConflict` is -returned, inspect active patches and revert the conflicting one before retrying. - -Do not use patches for business state, persistent data, or arbitrary method -invocation. App relaunch, rebuild, or Runtime replacement clears the live patch -state, but final verification must still confirm that no patch remains. diff --git a/skills/astrolabe/references/ui-graph.md b/skills/astrolabe/references/ui-graph.md deleted file mode 100644 index 15954d4..0000000 --- a/skills/astrolabe/references/ui-graph.md +++ /dev/null @@ -1,80 +0,0 @@ -# UI Graph Inspection - -Read this reference for ownership, cross-tree, mask, backing-layer, or another -relation that ordinary hierarchy inspection cannot express efficiently. - -## Entry Gate - -Two independent gates must pass: - -1. The installed Host/MCP exposes the `query_ui_graph` Tool. -2. The selected App advertises `uiGraphRelations`. - -If the Tool is unavailable while the capability is present, report a Host/MCP -installation or version gap. If the Tool exists but the capability is absent, -report a Runtime capability gap. Do not collapse these failures. - -Use hierarchy directly for ordinary parent-child inspection. Use UI Graph when -the question concerns non-hierarchy ownership or when a bounded, uniform -relation traversal materially helps. Derived `tree.viewChild` and -`tree.layerChild` remain available, but they do not require the Runtime to -duplicate authoritative hierarchy edges. - -## Frozen Workflow - -1. Run `inspect_screen` and retain its `snapshotId`. -2. When the root is omitted from the compact result, call `capture_hierarchy` - with the same `snapshotId`; do not recapture the current screen. -3. Select `rootOid` from that frozen hierarchy. -4. Call `query_ui_graph` with the same `appId` and `snapshotId`, the narrowest - useful `relationTypes`, and the required direction. -5. Read `truncated`, `truncationReasons`, `frontierOids`, and - `omittedFrontierCount` before expanding. -6. Inspect relevant returned nodes with `summarize_node_detail` under the same - snapshot. - -Use `outgoing` for relations owned by the root, `incoming` to locate owners, and -`both` only when the question genuinely needs both directions. Continue from a -frontier only when that branch matters; never request an unbounded graph. - -## Platform-Scoped Examples - -UIKit backing and descendant Layer traversal: - -```text -UIView --ios.view.backingLayer--> CALayer --tree.layerChild--> CALayer -``` - -UIKit mask ownership: - -```text -UIView --ios.view.backingLayer--> CALayer --ios.layer.mask--> CAShapeLayer -``` - -The mask edge proves object identity and ownership. Shape path, fill rule, and -other drawing semantics belong to node details rather than relation extensions. - -Android View hierarchy through the uniform relation API: - -```text -ViewGroup --tree.viewChild--> View -``` - -Do not assume that a platform-scoped relation exists on another Runtime, and do -not invent platform-specific MCP Tool names. - -## Recovery - -- `ui_graph_node_not_found`: verify that `rootOid` came from the same frozen - snapshot. Do not substitute a current node under the old snapshot. -- `invalid_ui_graph_snapshot`: capture one fresh hierarchy and retry the full - chain once. If it repeats, report a Host/Runtime graph-contract defect with - both versions; do not keep retrying or infer missing edges. -- Snapshot expired or mismatched: restart from screen inspection and keep every - subsequent node and relation on the new snapshot. -- Missing capability: stay read-only and report the Runtime recovery suggestion. -- Truncated result: report reasons and frontier metadata, then expand only a - branch required by the question. - -Report the exact relation types, direction, root, returned path, and any -truncation. Unknown or malformed topology cannot produce `passed`. diff --git a/skills/astrolabe/references/visual-regression.md b/skills/astrolabe/references/visual-regression.md deleted file mode 100644 index 94afade..0000000 --- a/skills/astrolabe/references/visual-regression.md +++ /dev/null @@ -1,39 +0,0 @@ -# Visual And Baseline Verification - -Read this reference when screenshots, design matching, pixel differences, or -reusable baselines matter. - -## Screenshot Evidence - -- Use `capture_screenshot` with `source: "auto"` unless a specific virtual or - physical target is required. -- Screenshots capture the latest screen and do not accept `snapshotId`. Never - claim pixel synchronization with an older hierarchy snapshot. -- Runtime frames and spacing use logical units. Screenshot dimensions and masks - use pixels. Convert only with the reported logical-to-pixel scale. -- Do not make pixel conclusions from a low-resolution screenshot unless the - task explicitly accepts that limitation. -- Pair screenshots with Runtime details when exact geometry, content mapping, - clipping, or style values affect the verdict. - -## Match A Design - -1. Freeze the hierarchy and locate each real content-bearing node. -2. Verify exact Runtime geometry and semantic styles. -3. Apply the rendered-content evidence gate before approving containment or - apparent size. -4. Capture a latest screenshot for composition. -5. Use `inspect_diff` when an expected image exists and the cause matters. -6. Inspect affected nodes before changing source. -7. After implementation, relaunch, refresh identifiers, and repeat. - -## Baselines - -1. Record a baseline only from an accepted, stable state. -2. Keep device, viewport, appearance, scale, and stable content consistent. -3. Use `compare_baseline` for repeatable pass/fail and `inspect_diff` for causes. -4. Re-record only when the intended design changed, never to hide a regression. - -For dynamic clocks, timers, avatars, names, and similar content, prefer stable -node-query ignore regions. Use named masks for known system UI. Ignore only the -smallest justified region because broad masks can hide real defects. diff --git a/test/review-contract.test.mjs b/test/review-contract.test.mjs index f71e309..d8ff319 100644 --- a/test/review-contract.test.mjs +++ b/test/review-contract.test.mjs @@ -55,222 +55,79 @@ test("README command examples match the current positional CLI contract", async assert.doesNotMatch(readme, /node-detail --app /); }); -test("Astrolabe skill verifies image rendering beyond the container frame", async () => { +test("Astrolabe skill stays concise and self-contained", async () => { const skill = await readProjectFile("skills/astrolabe/SKILL.md"); + const skillEntries = await readdir(resolve(projectRoot, "skills/astrolabe")); - assert.match(skill, /imageSize/); - assert.match(skill, /imageScale/); - assert.match(skill, /contentMode/); - assert.match(skill, /frame or bounds alone/); - assert.match(skill, /unscaled content mode/); - assert.doesNotMatch(skill, /always use `?scaleAspectFit`?/i); + assert.ok(skill.split("\n").length <= 100); + assert.deepEqual( + skillEntries.filter((entry) => entry !== ".DS_Store").sort(), + ["SKILL.md", "agents", "evals"] + ); + assert.doesNotMatch(skill, /references\//); }); -test("Astrolabe skill is platform-neutral and capability-driven", async () => { +test("Astrolabe skill gates inspection on a running compatible target", async () => { const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - assert.match(skill, /supported mobile platform/i); - assert.match(skill, /platform/); - assert.match(skill, /capabilities/); - assert.match(skill, /logical/); - assert.match(skill, /pixel/); - assert.match(skill, /snapshotId/); + assert.match(skill, /running and remain in the foreground/i); + assert.match(skill, /list_apps[\s\S]*compatibility\.status[\s\S]*inspect_screen/i); assert.match(skill, /recoverySuggestion/); - assert.doesNotMatch(skill, /implementing, debugging, or reviewing an iOS/i); - assert.doesNotMatch(skill, /`ios_[a-z_]+`|`android_[a-z_]+`/); + assert.match(skill, /build[\s\S]*unit tests[\s\S]*install(?:ed|ation)[\s\S]*killed/i); + assert.match(skill, /do not click, scroll, type/i); + assert.match(skill, /routes or deep links/i); + assert.match(skill, /wait for the developer/i); }); -test("Astrolabe skill keeps UI graph inspection bounded to one frozen snapshot", async () => { +test("Astrolabe skill defines the basic MCP and snapshot lifecycle", async () => { const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - assert.match(skill, /uiGraphRelations/); - assert.match( - skill, - /capture_hierarchy[\s\S]*query_ui_graph[\s\S]*summarize_node_detail/ - ); - assert.match(skill, /truncationReasons/); - assert.match(skill, /frontierOids/); - assert.match(skill, /omittedFrontierCount/); + for (const tool of [ + "list_apps", + "inspect_screen", + "find_nodes", + "inspect_node", + "summarize_node_detail", + "capture_screenshot", + "check_node", + "check_node_detail", + "check_style", + "check_layout" + ]) { + assert.match(skill, new RegExp(`\\b${tool}\\b`)); + } assert.match(skill, /same `appId` and `snapshotId`/); - assert.doesNotMatch(skill, /`ios_query_ui_graph`|`android_query_ui_graph`/); + assert.match(skill, /screenshot[\s\S]*latest[\s\S]*not guaranteed.*snapshot/i); + assert.match(skill, /restarted, killed, or reinstalled[\s\S]*discard[\s\S]*appId[\s\S]*snapshotId/i); + assert.match(skill, /target state changes[\s\S]*`appId` may stay the same[\s\S]*old snapshot/i); }); -test("Astrolabe skill loads advanced inspection guidance progressively", async () => { +test("Astrolabe skill reviews supplied design references with traceable evidence", async () => { const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - assert.ok( - skill.split("\n").length <= 260, - "core skill should stay bounded while advanced workflows live in references" - ); - assert.match(skill, /references\/rendered-content\.md/); - assert.match(skill, /references\/ui-graph\.md/); - assert.match(skill, /references\/visual-regression\.md/); - assert.match(skill, /references\/temporary-patches\.md/); + assert.match(skill, /design tool files, images, annotations, visual specifications, design systems/i); + assert.match(skill, /text, font family, font size, font weight, line height/i); + assert.match(skill, /position, width, height, alignment, relative spacing/i); + assert.match(skill, /corner radius, borders, shadows, clipping/i); + assert.match(skill, /`appId`, `snapshotId`, and node `oid`/); + assert.match(skill, /Design requirement.*Runtime actual.*Evidence.*Result/i); }); -test("Astrolabe skill gates design verification on target readiness and complete design evidence", async () => { - const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - const guidance = await readProjectFile( - "skills/astrolabe/references/design-verification.md" - ).catch(() => ""); - const evals = await readProjectFile("skills/astrolabe/evals/evals.json").catch(() => ""); - - assert.ok(skill.split("\n").length <= 260); - assert.match(skill, /design-verification\.md/); - assert.match(skill, /Target State Readiness/i); - assert.match(skill, /fresh[\s\S]*formal acceptance[\s\S]*snapshot/i); - assert.match(skill, /before readiness.*do not issue.*passed.*failed.*inconclusive/is); - assert.match(skill, /visible normal UI/i); - assert.match(skill, /App Router|private initializer|unapproved deep link/i); - assert.match(skill, /presentation-only/i); - assert.match(skill, /snapshot.*preliminary.*discard|discard.*preliminary.*snapshot/is); - assert.match(skill, /Design Expectations.*Coverage Ledger.*before.*checks/is); - assert.match(skill, /formal acceptance.*after readiness.*passed.*failed.*inconclusive/is); - - assert.match(guidance, /Design source priority/i); - assert.match(guidance, /Target Context/i); - assert.match(guidance, /Design Expectation/i); - assert.match(guidance, /current Target Context.*current device.*sufficient/is); - assert.match(guidance, /do not\s+automatically.*another device/is); - assert.match(guidance, /Other viewports.*neither blocks.*passed.*inconclusive/is); - for (const policy of [ - "exact", - "minimum", - "maximum", - "range", - "relation", - "derived", - "conditional" - ]) { - assert.match(guidance, new RegExp(`\\b${policy}\\b`, "i")); - } - assert.match(guidance, /tolerance.*measurement error/i); - assert.match(guidance, /fixed.*flexible|flexible.*fixed/is); - assert.match(guidance, /1\s*\/\s*displayScale/i); - assert.match(guidance, /one physical\s+pixel/i); - assert.match(guidance, /raw `actual`/i); - assert.match(guidance, /never.*layout flexibility/is); - assert.match(guidance, /unique node|coordinate.*evidence/i); - assert.match(guidance, /Coverage Ledger/i); - assert.match(guidance, /screenshot.*contradiction|contradiction.*screenshot/is); - assert.match(guidance, /required failed.*failed/is); - assert.match(guidance, /required inconclusive.*unchecked.*inconclusive/is); - assert.match(guidance, /notApplicable/); - assert.match(guidance, /authoritative contract.*does not apply.*current conditions/is); - assert.match(guidance, /notApplicable.*neither.*unchecked.*blocks.*passed/is); - assert.match(guidance, /20x20/); - assert.match(guidance, /leading.*15/i); - assert.match(guidance, /Avatar-to-Title.*10/i); - assert.match(guidance, /Title-to-CallIcon.*minimum.*10/i); - assert.match(guidance, /centerY/i); - - const evalCatalog = JSON.parse(evals); - assert.ok(Array.isArray(evalCatalog.evals)); - const evalsByID = new Map(evalCatalog.evals.map((entry) => [entry.id, entry])); - for (const id of [ - "fixed-spacing-failure", - "legal-adaptive-spacing", - "missing-adaptive-contract", - "ambiguous-selector", - "image-center-mode-overflow", - "target-absent-ios-physical-device", - "authorized-mock-presentation-only", - "exact-policy-pass", - "exact-policy-fail", - "minimum-policy-pass", - "minimum-policy-fail", - "maximum-policy-pass", - "maximum-policy-fail", - "range-policy-pass", - "range-policy-fail", - "relation-policy-pass", - "relation-policy-fail", - "derived-policy-pass", - "derived-policy-fail", - "conditional-policy-pass", - "conditional-policy-fail", - "contact-cell-scale2-quantization-fixed-gap-regression" - ]) { - assert.ok(evalsByID.has(id), `missing stable eval: ${id}`); - } +test("Astrolabe skill metadata and evals match the simplified contract", async () => { + const metadata = await readProjectFile("skills/astrolabe/agents/openai.yaml"); + const evalCatalog = JSON.parse(await readProjectFile("skills/astrolabe/evals/evals.json")); + + assert.match(metadata, /running mobile app UI/i); + assert.match(metadata, /\$astrolabe/); + assert.equal(evalCatalog.skill_name, "astrolabe"); + assert.deepEqual(evalCatalog.evals.map((entry) => entry.id), [1, 2]); for (const entry of evalCatalog.evals) { assert.equal(typeof entry.prompt, "string"); assert.equal(typeof entry.expected_output, "string"); - assert.ok(Array.isArray(entry.files)); + assert.deepEqual(entry.files, []); + assert.ok(Array.isArray(entry.expectations)); + assert.ok(entry.expectations.length > 0); } - assert.match(evalsByID.get("image-center-mode-overflow").prompt, /27x27/); - assert.match(evalsByID.get("image-center-mode-overflow").prompt, /54x54/); - assert.match(evalsByID.get("image-center-mode-overflow").prompt, /fully contained/i); - const quantizationEval = evalsByID.get( - "contact-cell-scale2-quantization-fixed-gap-regression" - ); - assert.match(quantizationEval.prompt, /displayScale 2/i); - assert.match(quantizationEval.prompt, /-0\.25/); - assert.match(quantizationEval.prompt, /Avatar-to-Title.*18/i); -}); - -test("Astrolabe skill separates rendered content from its layout container", async () => { - const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - const guidance = await readProjectFile( - "skills/astrolabe/references/rendered-content.md" - ).catch(() => ""); - const contract = `${skill}\n${guidance}`; - - assert.match(contract, /layout box/i); - assert.match(contract, /rendered footprint/i); - assert.match(contract, /intrinsic content/i); - assert.match(contract, /mapping policy/i); - assert.match(contract, /clipping or masking/i); - assert.match(contract, /transform or visual effect/i); - assert.match(contract, /cannot produce `passed`/i); - assert.match(contract, /imageSize/); - assert.match(contract, /contentMode/); -}); - -test("Astrolabe skill distinguishes UI graph tool and Runtime failures", async () => { - const guidance = await readProjectFile( - "skills/astrolabe/references/ui-graph.md" - ).catch(() => ""); - - assert.match(guidance, /Tool is unavailable/i); - assert.match(guidance, /uiGraphRelations/); - assert.match(guidance, /invalid_ui_graph_snapshot/); - assert.match(guidance, /ui_graph_node_not_found/); - assert.match(guidance, /capture_hierarchy.*same `snapshotId`/is); -}); - -test("Astrolabe skill metadata covers rendered content and UI relations", async () => { - const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - const metadata = await readProjectFile( - "skills/astrolabe/agents/openai.yaml" - ); - - assert.match(skill.slice(0, skill.indexOf("---", 4)), /rendered content/i); - assert.match(skill.slice(0, skill.indexOf("---", 4)), /UI relations/i); - assert.match(metadata, /rendered content/i); - assert.match(metadata, /UI relations/i); -}); - -test("Astrolabe skill does not treat node details as an atomic hierarchy snapshot", async () => { - const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - - assert.match(skill, /detailSource/); - assert.match(skill, /snapshotCache/); - assert.match(skill, /liveRuntime/); - assert.match(skill, /detailCapturedAtUnixTime/); - assert.match(skill, /not.*atomic|not.*same capture time/is); -}); - -test("Astrolabe skill rejects absence conclusions from a truncated hierarchy", async () => { - const skill = await readProjectFile("skills/astrolabe/SKILL.md"); - - assert.match(skill, /summarize_hierarchy/); - assert.match(skill, /maxDepth/); - assert.match(skill, /nodeCount/); - assert.match(skill, /returnedNodeCount/); - assert.match(skill, /omittedNodeCount/); - assert.match(skill, /truncated/); - assert.match(skill, /not.*(?:absent|missing).*truncated/is); }); test("public documentation advertises delivered Android View support", async () => {