Skip to content

Latest commit

 

History

History
329 lines (236 loc) · 13.8 KB

File metadata and controls

329 lines (236 loc) · 13.8 KB

Inspect Screen and Diagnostics Guide

What inspectScreen is

inspectScreen() turns a screen definition into a serializable semantic graph snapshot.

It is for debugging, documentation, tests, diagnostics, and future tooling (DevTools, analytics, code generation).

It is not a renderer. It does not produce DOM, test output, or server responses. It only shows the current state of a screen's semantic nodes and their diagnostics.

The function is exported from @intent-framework/core.

Minimal usage

import { inspectScreen } from "@intent-framework/core"
import { InviteMember } from "./InviteMember.js"

const graph = inspectScreen(InviteMember)
console.log(JSON.stringify(graph, null, 2))

The canonical example at examples/canonical-invite does exactly this — it renders the screen to DOM and displays inspectScreen() output in a <pre> element on the same page:

const graph = inspectScreen(InviteMember)
inspect.textContent = JSON.stringify(graph, null, 2)

What the graph contains

The returned InspectedScreen object contains:

Field Type Description
name string Screen name from screen("Name", ...)
semanticId string Stable screen-level identifier
asks InspectedAsk[] Every ask node with current state
acts InspectedAct[] Every action node with current state
flows InspectedFlow[] Every flow with step count
surfaces InspectedSurface[] Every surface with item count
resources InspectedResource[] Every resource node (requires runtime resources)
diagnostics GraphDiagnostic[] Diagnostics for this screen

Each ask includes: id, semanticId, label, kind, required, isPrivate, valid, error.

Each action includes: id, semanticId, label, primary, enabled, blockedReasons, status, statusMessage, invalidates.

Each resource includes: id, semanticId, name, status, hasValue, stale, error.

For a detailed explanation of resource lifecycle, runtime scoping, and invalidation, see the Resources Guide.

Resources require passing runtime resource nodes:

const runtime = createScreenRuntime(MyScreen)
await runtime.start()
const graph = inspectScreen(MyScreen, runtime.resources)

Semantic IDs

Semantic IDs are stable, deterministic identifiers generated from node labels.

Every node kind has a prefix:

Node kind Prefix Example label semanticId
Screen screen: InviteMember screen:invite-member
Ask ask: Email ask:email
Action action: Invite member action:invite-member
Flow flow: login flow:login
Surface surface: main surface:main
Resource resource: team resource:team

Rules

  • Deterministic: the same label produces the same semantic ID every time, across any number of inspectScreen() calls.
  • Duplicate labels get numeric suffixes: ask:email, ask:email-2, ask:email-3. The first occurrence keeps the unadorned ID.
  • Normalization: labels are lowercased, whitespace becomes hyphens, and punctuation/symbols are stripped. "Email!" becomes ask:email.
  • Symbol-only labels (e.g. "!!!") produce numeric fallback IDs scoped to their kind: action:1, action:2.
  • Unrelated screens do not affect each other's semantic IDs.

Important

These IDs are for inspection, diagnostics, and future tooling. They are not:

  • Public DOM id or data-* attributes (the DOM renderer uses its own naming conventions)
  • User-facing strings
  • Stable across screen definition changes (adding a new ask before an existing one can shift suffixes)

Diagnostics available today

Current diagnostics codes, their severity, and meaning:

multiple-primary-actions

  • Severity: warning
  • What it means: The screen defines more than one primary action. When multiple actions are marked .primary(), the default action behavior (Enter key) is ambiguous — the renderer does not know which one to trigger.
  • Triggers when: Two or more actions call .primary() in the same screen.
  • Fix: Designate at most one action as primary, or remove .primary() from the extras.

Example trigger:

const save = $.act("Save").primary()
const delete = $.act("Delete").primary()

secret-ask-not-private

  • Severity: warning
  • What it means: An ask is marked .asSecret() but not .private(). Secret values (passwords, tokens) should also be marked private so they are excluded from debug snapshots, logs, and future tooling output.
  • Triggers when: .asSecret() is called without .private().
  • Fix: Add .private() to the ask, or remove .asSecret() if the value is not sensitive.

Example:

$.ask("Password", password).asSecret()        // triggers diagnostic
$.ask("Password", password).asSecret().private() // no diagnostic

primary-action-without-blocked-reason

  • Severity: info
  • What it means: A primary action has one or more blocking conditions (.when(condition)) but none of those conditions provide a human-readable blocked reason. Users will see the action disabled but will not know why.
  • Triggers when: A primary action calls .when(condition) without the second argument, and no condition in that action provides a message.
  • Fix: Add a blocked-reason string to each .when() call.

Example:

$.act("Submit").primary().when(emailAsk.valid) // triggers diagnostic
$.act("Submit").primary().when(emailAsk.valid, "Enter a valid email.") // no diagnostic

ask-not-in-surface

  • Severity: warning
  • What it means: An ask node exists in the screen definition but is not included in any surface. Screens that do not surface their asks will not render those asks — they exist in the graph but are invisible in the DOM.
  • Triggers when: A $.ask(...) is defined but never added to a $.surface(...).
  • Fix: Add the ask to a surface.

Example:

$.ask("Email", email)          // triggers diagnostic
$.surface("main").contains()   // ask not included

action-not-in-surface

  • Severity: warning
  • What it means: Same as ask-not-in-surface but for action nodes. An action exists in the screen but is not surfaced.
  • Triggers when: A $.act(...) is defined but never added to a $.surface(...).
  • Fix: Add the action to a surface.

Example:

$.act("Hidden")                // triggers diagnostic
$.surface("main").contains()   // action not included

surfaced-node-not-in-any-flow

  • Severity: info
  • What it means: An ask or action is included in a surface but is not referenced by any flow step. The node is visible but there is no defined interaction path leading to it.
  • Triggers when: At least one flow exists on the screen, and a surfaced ask or action is not referenced in any flow's .startsWith() / .then() chain.
  • Why only when flows exist: Flows are optional. A screen with zero flows relies on implicit DOM ordering and is not expected to reference every surfaced node in a flow.
  • Fix: Add the node to a flow step, or remove the node from the surface if it should not be rendered.

Example:

const email = $.ask("Email", emailState)
const name = $.ask("Name", nameState)

$.surface("main").contains(email, name)

$.flow("signup").startsWith(email)  // name is surfaced but not in any flow
// Diagnostic: surfaced-node-not-in-any-flow for "Name"

flow-step-not-surfaced

  • Severity: warning
  • What it means: A flow step references an ask or action node that is not included in any surface. The interaction path is defined but one of its steps would render nothing visible, making the flow incomplete.
  • Triggers when: A flow's .startsWith() or .then() references a node that is not in any .surface().contains().
  • Why per-flow and not deduplicated: Unlike ask-not-in-surface / action-not-in-surface (which fire once per unsurfaced node), this diagnostic fires once per (flow, step) pair. If the same unsurfaced node appears in multiple flows, each one is reported so the author knows which flows are broken.
  • Fix: Add the missing node to a surface, or remove it from the flow step.

Example:

const email = $.ask("Email", emailState)
$.surface("main").contains()        // email not surfaced
$.flow("signup").startsWith(email)  // flow-step-not-surfaced for "Email" in flow "signup"

orphaned-flow

  • Severity: warning
  • What it means: A flow exists with one or more steps, but none of its steps reference a node that appears in any surface. The flow defines an interaction path that is entirely disconnected from the rendered UI.
  • Triggers when: A flow has at least one step, and every step references an ask or action that is not in any surface.
  • Does not trigger for: Empty flows (zero-step flows are not diagnosed, as they may be placeholders for future steps).
  • Relationship to flow-step-not-surfaced: orphaned-flow is a flow-level signal — it fires once per affected flow. The individual unsurfaced steps also each fire flow-step-not-surfaced. A flow with all-unsurfaced steps will produce both diagnostics.
  • Fix: Surface at least one node referenced by a flow step, or add the flow's steps to a surface.

Example:

const email = $.ask("Email", emailState)
const name = $.ask("Name", nameState)
$.surface("main").contains()              // no nodes surfaced
$.flow("signup").startsWith(email).then(name)  // orphaned-flow for "signup"

Diagnostic output format

Each diagnostic is a GraphDiagnostic object:

{
  severity: "warning",
  code: "secret-ask-not-private",
  message: "Secret ask should also be marked private.",
  nodeId: "ask_password",
  semanticNodeId: "ask:password",
  flow?: FlowDiagnosticMeta
}
  • severity: "info", "warning", or "error" (errors not yet used).
  • code: a machine-readable string suitable for filtering.
  • message: human-readable explanation.
  • nodeId (optional): the internal node ID of the affected node.
  • semanticNodeId (optional): the semantic ID of the affected node, when applicable.
  • flow (optional): a FlowDiagnosticMeta object present on flow-scoped diagnostics (flow-step-not-surfaced, orphaned-flow).

FlowDiagnosticMeta type

type FlowDiagnosticMeta = {
  flowNodeId: string
  flowSemanticNodeId?: string
}

FlowDiagnosticMeta identifies the containing flow for a diagnostic:

Field Type Description
flowNodeId string Internal node ID of the flow
flowSemanticNodeId string (optional) Semantic ID of the flow, populated by inspectScreen()

Present on diagnostics where the issue is scoped to a specific flow:

Diagnostic nodeId / semanticNodeId flow / flow.flowNodeId
flow-step-not-surfaced The unsurfaced ask or action step The containing flow
orphaned-flow Undefined (no single node is at fault) The orphaned flow

Development workflow

During screen authoring

While building a screen, call inspectScreen() to verify the graph structure:

const graph = inspectScreen(MyScreen)
console.log(graph.diagnostics)

This catches common mistakes before rendering: unsurfaced nodes, missing blocked reasons, ambiguous primary actions.

In a debug panel

The canonical example and web-basic demo both display inspectScreen() output on the page. The demo's diagnostics panel calls inspectScreen(screenDef) and formats the diagnostics array:

const inspected = inspectScreen(screenDef)
if (inspected.diagnostics.length === 0) {
  el.textContent = "✓ No diagnostics."
} else {
  el.textContent = inspected.diagnostics.map(d =>
    `[${d.severity}] ${d.code}${d.nodeId ? ` (${d.nodeId})` : ""}: ${d.message}`
  ).join("\n")
}

See examples/web-basic/src/demo-panels.ts for the full pattern.

In tests

Assert diagnostic presence in your test suite:

import { inspectScreen } from "@intent-framework/core"

test("no diagnostics for clean screen", () => {
  const inspected = inspectScreen(MyScreen)
  expect(inspected.diagnostics).toEqual([])
})

test("reports missing surface membership", () => {
  const inspected = inspectScreen(MyScreen)
  const unsurfaced = inspected.diagnostics.filter(
    d => d.code === "ask-not-in-surface"
  )
  expect(unsurfaced).toHaveLength(0)
})

Semantic IDs in docs and snapshots

Semantic IDs are stable for the same screen definition, so they can appear in documentation and test snapshots. However, be aware that changes to label ordering (adding a duplicate label before an existing one) shifts numeric suffixes.

Boundaries

The current graph inspection has deliberate limitations:

  • No full reachability analysis. Diagnostics check surface membership but do not trace whether all nodes are reachable through flows or surfaces.
  • No route-wide graph inspection. inspectScreen() inspects one screen at a time. There is no cross-screen or route-level graph snapshot yet.
  • No DevTools package yet. There is no dedicated browser extension or DevTools panel. The web-basic demo uses a MutationObserver-driven DOM side panel.
  • DOM renderer exposes semantic IDs as opt-in data attributes. Pass showSemanticIds: true to renderDom() to add data-intent-screen, data-intent-ask, and data-intent-action attributes to rendered DOM elements. Default output is unchanged.
  • Resource graph inspection exists. inspectScreen() accepts runtime resource nodes and reports status, staleness, and errors. However, cache and staleness semantics are still early — there is no automatic reload, polling, or TTL-based invalidation.
  • No visual diff or time-travel debugging. Graph snapshots are static. There is no history of state changes over time.