Skip to content

Commit 47b81cc

Browse files
authored
Merge pull request #56 from intent-framework/docs/inspect-screen-diagnostics-guide
docs: add inspectScreen diagnostics guide
2 parents e204d17 + 01253ae commit 47b81cc

4 files changed

Lines changed: 258 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ Diagnostics currently catch graph-level issues such as:
295295

296296
Diagnostics are not lint rules for HTML. They are semantic graph feedback.
297297

298+
For a detailed walkthrough of diagnostics, semantic IDs, and development workflow, see the [Inspect Screen and Diagnostics Guide](docs/Inspect-Screen.md).
299+
298300
## Development
299301

300302
Run clean validation locally:

docs/Inspect-Screen.md

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# Inspect Screen and Diagnostics Guide
2+
3+
## What `inspectScreen` is
4+
5+
`inspectScreen()` turns a screen definition into a serializable semantic graph snapshot.
6+
7+
It is for debugging, documentation, tests, diagnostics, and future tooling (DevTools, analytics, code generation).
8+
9+
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.
10+
11+
The function is exported from `@intent-framework/core`.
12+
13+
## Minimal usage
14+
15+
```ts
16+
import { inspectScreen } from "@intent-framework/core"
17+
import { InviteMember } from "./InviteMember.js"
18+
19+
const graph = inspectScreen(InviteMember)
20+
console.log(JSON.stringify(graph, null, 2))
21+
```
22+
23+
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:
24+
25+
```ts
26+
const graph = inspectScreen(InviteMember)
27+
inspect.textContent = JSON.stringify(graph, null, 2)
28+
```
29+
30+
## What the graph contains
31+
32+
The returned `InspectedScreen` object contains:
33+
34+
| Field | Type | Description |
35+
|-------|------|-------------|
36+
| `name` | `string` | Screen name from `screen("Name", ...)` |
37+
| `semanticId` | `string` | Stable screen-level identifier |
38+
| `asks` | `InspectedAsk[]` | Every ask node with current state |
39+
| `acts` | `InspectedAct[]` | Every action node with current state |
40+
| `flows` | `InspectedFlow[]` | Every flow with step count |
41+
| `surfaces` | `InspectedSurface[]` | Every surface with item count |
42+
| `resources` | `InspectedResource[]` | Every resource node (requires runtime resources) |
43+
| `diagnostics` | `GraphDiagnostic[]` | Diagnostics for this screen |
44+
45+
Each ask includes: `id`, `semanticId`, `label`, `kind`, `required`, `isPrivate`, `valid`, `error`.
46+
47+
Each action includes: `id`, `semanticId`, `label`, `primary`, `enabled`, `blockedReasons`, `status`, `statusMessage`, `invalidates`.
48+
49+
Each resource includes: `id`, `semanticId`, `name`, `status`, `hasValue`, `stale`, `error`.
50+
51+
Resources require passing runtime resource nodes:
52+
53+
```ts
54+
const runtime = createScreenRuntime(MyScreen)
55+
await runtime.start()
56+
const graph = inspectScreen(MyScreen, runtime.resources)
57+
```
58+
59+
## Semantic IDs
60+
61+
Semantic IDs are stable, deterministic identifiers generated from node labels.
62+
63+
Every node kind has a prefix:
64+
65+
| Node kind | Prefix | Example label | semanticId |
66+
|-----------|--------|---------------|------------|
67+
| Screen | `screen:` | `InviteMember` | `screen:invite-member` |
68+
| Ask | `ask:` | `Email` | `ask:email` |
69+
| Action | `action:` | `Invite member` | `action:invite-member` |
70+
| Flow | `flow:` | `login` | `flow:login` |
71+
| Surface | `surface:` | `main` | `surface:main` |
72+
| Resource | `resource:` | `team` | `resource:team` |
73+
74+
### Rules
75+
76+
- **Deterministic**: the same label produces the same semantic ID every time, across any number of `inspectScreen()` calls.
77+
- **Duplicate labels** get numeric suffixes: `ask:email`, `ask:email-2`, `ask:email-3`. The first occurrence keeps the unadorned ID.
78+
- **Normalization**: labels are lowercased, whitespace becomes hyphens, and punctuation/symbols are stripped. `"Email!"` becomes `ask:email`.
79+
- **Symbol-only labels** (e.g. `"!!!"`) produce numeric fallback IDs scoped to their kind: `action:1`, `action:2`.
80+
- **Unrelated screens** do not affect each other's semantic IDs.
81+
82+
### Important
83+
84+
These IDs are for inspection, diagnostics, and future tooling. They are not:
85+
86+
- Public DOM `id` or `data-*` attributes (the DOM renderer uses its own naming conventions)
87+
- User-facing strings
88+
- Stable across screen definition changes (adding a new ask before an existing one can shift suffixes)
89+
90+
## Diagnostics available today
91+
92+
Current diagnostics codes, their severity, and meaning:
93+
94+
### `multiple-primary-actions`
95+
96+
- **Severity**: `warning`
97+
- **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.
98+
- **Triggers when**: Two or more actions call `.primary()` in the same screen.
99+
- **Fix**: Designate at most one action as primary, or remove `.primary()` from the extras.
100+
101+
Example trigger:
102+
103+
```ts
104+
const save = $.act("Save").primary()
105+
const delete = $.act("Delete").primary()
106+
```
107+
108+
### `secret-ask-not-private`
109+
110+
- **Severity**: `warning`
111+
- **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.
112+
- **Triggers when**: `.asSecret()` is called without `.private()`.
113+
- **Fix**: Add `.private()` to the ask, or remove `.asSecret()` if the value is not sensitive.
114+
115+
Example:
116+
117+
```ts
118+
$.ask("Password", password).asSecret() // triggers diagnostic
119+
$.ask("Password", password).asSecret().private() // no diagnostic
120+
```
121+
122+
### `primary-action-without-blocked-reason`
123+
124+
- **Severity**: `info`
125+
- **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.
126+
- **Triggers when**: A primary action calls `.when(condition)` without the second argument, and no condition in that action provides a message.
127+
- **Fix**: Add a blocked-reason string to each `.when()` call.
128+
129+
Example:
130+
131+
```ts
132+
$.act("Submit").primary().when(emailAsk.valid) // triggers diagnostic
133+
$.act("Submit").primary().when(emailAsk.valid, "Enter a valid email.") // no diagnostic
134+
```
135+
136+
### `ask-not-in-surface`
137+
138+
- **Severity**: `warning`
139+
- **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.
140+
- **Triggers when**: A `$.ask(...)` is defined but never added to a `$.surface(...)`.
141+
- **Fix**: Add the ask to a surface.
142+
143+
Example:
144+
145+
```ts
146+
$.ask("Email", email) // triggers diagnostic
147+
$.surface("main").contains() // ask not included
148+
```
149+
150+
### `action-not-in-surface`
151+
152+
- **Severity**: `warning`
153+
- **What it means**: Same as `ask-not-in-surface` but for action nodes. An action exists in the screen but is not surfaced.
154+
- **Triggers when**: A `$.act(...)` is defined but never added to a `$.surface(...)`.
155+
- **Fix**: Add the action to a surface.
156+
157+
Example:
158+
159+
```ts
160+
$.act("Hidden") // triggers diagnostic
161+
$.surface("main").contains() // action not included
162+
```
163+
164+
### Diagnostic output format
165+
166+
Each diagnostic is a `GraphDiagnostic` object:
167+
168+
```ts
169+
{
170+
severity: "warning",
171+
code: "secret-ask-not-private",
172+
message: "Secret ask should also be marked private.",
173+
nodeId: "ask_password",
174+
semanticNodeId: "ask:password"
175+
}
176+
```
177+
178+
- `severity`: `"info"`, `"warning"`, or `"error"` (errors not yet used).
179+
- `code`: a machine-readable string suitable for filtering.
180+
- `message`: human-readable explanation.
181+
- `nodeId` (optional): the internal node ID of the affected node.
182+
- `semanticNodeId` (optional): the semantic ID of the affected node, when applicable.
183+
184+
## Development workflow
185+
186+
### During screen authoring
187+
188+
While building a screen, call `inspectScreen()` to verify the graph structure:
189+
190+
```ts
191+
const graph = inspectScreen(MyScreen)
192+
console.log(graph.diagnostics)
193+
```
194+
195+
This catches common mistakes before rendering: unsurfaced nodes, missing blocked reasons, ambiguous primary actions.
196+
197+
### In a debug panel
198+
199+
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:
200+
201+
```ts
202+
const inspected = inspectScreen(screenDef)
203+
if (inspected.diagnostics.length === 0) {
204+
el.textContent = "✓ No diagnostics."
205+
} else {
206+
el.textContent = inspected.diagnostics.map(d =>
207+
`[${d.severity}] ${d.code}${d.nodeId ? ` (${d.nodeId})` : ""}: ${d.message}`
208+
).join("\n")
209+
}
210+
```
211+
212+
See `examples/web-basic/src/demo-panels.ts` for the full pattern.
213+
214+
### In tests
215+
216+
Assert diagnostic presence in your test suite:
217+
218+
```ts
219+
import { inspectScreen } from "@intent-framework/core"
220+
221+
test("no diagnostics for clean screen", () => {
222+
const inspected = inspectScreen(MyScreen)
223+
expect(inspected.diagnostics).toEqual([])
224+
})
225+
226+
test("reports missing surface membership", () => {
227+
const inspected = inspectScreen(MyScreen)
228+
const unsurfaced = inspected.diagnostics.filter(
229+
d => d.code === "ask-not-in-surface"
230+
)
231+
expect(unsurfaced).toHaveLength(0)
232+
})
233+
```
234+
235+
### Semantic IDs in docs and snapshots
236+
237+
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.
238+
239+
## Boundaries
240+
241+
The current graph inspection has deliberate limitations:
242+
243+
- **No full reachability analysis.** Diagnostics check surface membership but do not trace whether all nodes are reachable through flows or surfaces.
244+
- **No route-wide graph inspection.** `inspectScreen()` inspects one screen at a time. There is no cross-screen or route-level graph snapshot yet.
245+
- **No DevTools package yet.** There is no dedicated browser extension or DevTools panel. The web-basic demo uses a `MutationObserver`-driven DOM side panel.
246+
- **DOM renderer does not expose all semantic IDs as data attributes.** The DOM renderer uses its own `id` conventions for accessibility. Semantic IDs are not yet emitted as `data-semantic-id` attributes.
247+
- **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.
248+
- **No visual diff or time-travel debugging.** Graph snapshots are static. There is no history of state changes over time.

docs/MVP-Checkpoint.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,14 @@ Do not jump into these without a very specific narrow PR:
137137
## Proven
138138

139139
- ✅ Canonical example app (`examples/canonical-invite`) — new developers can clone and run after the quickstart. See [Canonical Invite Example](../examples/canonical-invite).
140+
-`inspectScreen()` diagnostics guide (`docs/Inspect-Screen.md`) — documents all five current diagnostics, semantic ID behavior, development workflow, and boundaries.
141+
142+
## Documented
143+
144+
-`inspectScreen()` diagnostic guide (`docs/Inspect-Screen.md`) — covers graph contents, all five current diagnostics, semantic ID behavior, development workflow, and current boundaries.
140145

141146
## Recommended next moves
142147

143-
1. Add a tiny diagnostics/dev-inspection page or command that prints `inspectScreen()` output for demo screens without `MutationObserver`.
148+
1. ~~Add a tiny diagnostics/dev-inspection page or command that prints `inspectScreen()` output for demo screens without `MutationObserver`.~~ (Covered by the diagnostic guide documenting the `demo-panels.ts` pattern.)
144149
2. Improve resource semantics documentation: runtime-scoped resources, reload, stale, invalidation, context.
145150
3. Add one or two graph diagnostics that catch real authoring mistakes before adding any new package.

docs/Quickstart.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,5 @@ Intent does not replace components for everything. It replaces components as the
226226
```
227227
2. See the [web demo](../examples/web-basic) for a full team invite flow with routing, resources, and diagnostics.
228228
3. Read the [Specification](Specification.md) for the architecture.
229-
4. Check the [MVP Checkpoint](MVP-Checkpoint.md) for current boundaries.
229+
4. Read the [Inspect Screen and Diagnostics Guide](Inspect-Screen.md) for a detailed walkthrough of `inspectScreen()`, diagnostics, and semantic IDs.
230+
5. Check the [MVP Checkpoint](MVP-Checkpoint.md) for current boundaries.

0 commit comments

Comments
 (0)