Full-page screenshots.
browser screenshot --full-page captures the whole scrollable page rather than the viewport, and capture --screenshot saves a full-page PNG beside each capture as <stamp>.snap.png (removed by capture prune along with its capture), so a view set keeps a picture of every state it recorded.Resolve privacy and watch from every component whose selector matches an element, as SEP-0009 and SEP-0015 now state, instead of from whichever component names it. A broader component naming an element (a Field child naming every input in a form) no longer discards a narrower component’s block, a view component sharing a global’s name no longer drops the global’s privacy or watch, two declarations on one element resolve to the strictest, and block is absolute: an unmask reopens a mask, never a block.ComponentMatch.Watched lists every watched component matching a node, and ComponentMatch.Watch reports whether any does. New on match.Matcher: Privacy and Watched resolve every node of a tree, including nodes no component names. match.Withholds is exported, so a consumer withholding properties it extracted itself applies the matcher’s rule.Selectors match exactly what a browser matches. Every test in a compound now applies instead of only the last: a repeated attribute name ([class*="a"][class*="b"]), a repeated #id (#a#a matches, #a#b matches nothing), and a second :is()/:where() (:is(.a):is(.b)). The parser rejects a type selector or * after other tokens ([x]div), which CSS does not allow. Matching compares the values of the attributes HTML lists as case-insensitive (type, rel, target, lang and the rest) ASCII case-insensitively, so input[type=password] matches type="PASSWORD"; in an HTML document that list applies to every element, SVG and MathML included. Type names fold with ASCII rules only, and a ~= value that is empty or contains whitespace never matches.File-root
definitions: (SEP-0019, draft): shared components that are $ref targets but never matched on their own. A view gets a definition only where it references one, scoped by the reference, so a structure whose selectors are only safe in context (a card’s img, h3, button) can be shared across views without becoming a global that claims every page. The loader, validate, lint, stats and the unknown-field check all know the key; a global wins a name clash (definition-shadowed-by-global), and a duplicate definition warns (merge-collision-definition).Also fixes $ref resolution for duplicate global names: the loader let the last same-name global (by source-file path) win, contradicting the spec’s first-seen rule (and the merge-collision-component warning’s own message). It now resolves to the first. Corpora with same-name globals that differ in selector may expand $ref differently; validate already warns on them.Snapshots and captures record the page URL. The header of a .snap now carries a url: line after route:, and viewset.URLOf reads it back, so an offline consumer can re-derive which view a capture belongs to (or match it against routes of its own) without the live page. Captures written earlier have no url: line; URLOf returns "" for them.Match attribute selectors against the camelCase SVG and MathML attribute names captures keep, so
[viewBox="0 0 24 24"] matches offline as it does in a browser.Skills: teach the fields added in 0.34.0.sightmap-authoring now covers privacy, watch and tags on components, URL properties on views and requests, and file-root environments and origins. It also corrects the message guidance: an uncaught exception arrives with level exception, so level: ERROR does not match it. sightmap-browser explains the view tags: line in a snapshot, and why a component under privacy: block or mask renders without its content properties.Add
privacy to component entries (SEP-0009).privacy: block | mask | unmask declares whether a capture consumer may retain the matched element’s content. It applies to the element and its subtree, and the nearest enclosing declaration wins, so the common shape is a mask on a form with an unmask on the one field inside it that is safe to keep.Resolution deliberately differs from tags, which union across every applicable definition. A union is right for classification, where more labels are additive; it is wrong for a directive, where two applicable values are a contradiction that has to be decided rather than merged.It governs extracted properties as well as captured content, at the node each value is read from rather than at the component that declared it. Extraction is tree-closed, so a PATH.prop reaching a blocked descendant is withheld even when the declaring component is unrestricted. exists: survives mask because presence is structure, as do the four interactive-state attributes SEP-0013 defines (checked, selected, disabled, expanded) — a closed set carrying state rather than a value, without which a masked form could report no control state at all.mask also covers attribute values in the captured recording, not only text and input values. A consumer must withhold every data-* attribute, value/title/alt/placeholder/aria-label, and any attribute the corpus names in an attr= extract anywhere — naming an attribute in an extract is itself the author declaring it carries content, which keeps the rule from going stale. Presentational attributes (class, style, id) may be retained because replay needs them to render, and the SEP states plainly that user data in one of those survives a mask and should use block instead. A withheld property is absent, indistinguishable from one that did not resolve.Omission declares nothing: a component with no privacy leaves the consumer’s own default untouched, so a corpus can adopt the field one component at a time. A consumer may withhold more than the corpus asks, but never less.The loader carries the authored value onto ComponentDef.Privacy verbatim and does not inherit it to children, matching tags/source/memory. Resolution is the consumer’s job, over the flattened chain.Add watch to component entries (SEP-0015).watch: true asks a capture consumer to report that a component became visible to the user, rather than reporting it only when someone interacts with it. The surrounding lifecycle (rendered, removed) is optional: becoming visible is the only moment that answers the question the field exists for, and the rest is largely churn, since an element can render far off-screen, re-render on every state change, and be removed by a route transition. Repeat reports of one appearance should be collapsed, and a visibility report is passive — never an interaction. The components whose appearance is the signal (an empty state, an error, a promotion) are usually the ones nobody clicks, so an interaction-shaped record contains no trace of them.watch applies to the component it is declared on and never to its children, the opposite of SEP-0009 privacy. Privacy is a restriction, where covering the subtree is the safe default; watch generates records, where covering a subtree silently would multiply them.A consumer’s interactivity heuristic must not suppress a watched component. Without that rule the field does nothing for exactly the non-interactive components that motivate it.The loader carries the authored value onto ComponentDef.Watch. watch: false is identical to omitting the field.Add named environments and origins (SEP-0014).A file-root environments array defines deploy targets. A web environment maps surface names (app, api) to the scheme://host[:port] URL each surface has there; a native one (platform: ios | android) is identified by app_id and an optional build_type, and borrows a web environment’s origins through backend. A file-root origins map holds shared origins, hosts that are the same in every environment such as a tracking pixel or a vendor endpoint, and an environment’s own entry of the same name overrides it. Origin URLs may be patterns, with wildcards only in the leftmost host label (https://deploy-preview-*--acme.netlify.app, https://**.acme.com) or the port (http://localhost:*), so preview deploys and local dev servers belong to an environment without being enumerated.Views and requests reference both by name through environments: [...] and origins: [...]. Absent or empty means unconstrained. A view-scoped request’s environments are intersected with its view’s; its origins are never inherited. The fields are declarative only and take no part in route matching, so adding a list to an existing entity never changes what traffic it matches.Both registries are project-wide, first file by source path wins. The Go SDK loads the new fields (defaulting platform to web), exposes Corpus.EnvironmentByName, Corpus.ResolveOrigin, and RequestEnvironments, and validates them with new diagnostics: environment-invalid, origin-invalid, environment-backend-invalid, environment-ref-unresolved, origin-ref-unresolved (errors), and environment-name-collision, origin-name-collision, origin-environment-gap, origin-host-shared, environment-duplicate, environments-empty, origins-empty (warnings).One extract object for every property (SEP-0017).Component, request and message properties now read with extract: { from, path, pattern, join }, drawing from from one namespace: dom.text, dom.raw_text, dom.attr, dom.state, component, component.exists, req.body/rsp.body/req.headers/rsp.headers, and stack. Components gain pattern, and a path segment written Name[] reads every match instead of the first, collapsed into one value by join. A bracket with content in a path segment is reserved for future path predicates. dom.state reads interactive state separately from the markup attribute of the same name.The string forms (extract: text, attr=NAME, PATH.prop, exists:PATH, and request/message source/field/pattern) still load, lowered to the object, and validate warns extract-legacy-form with the replacement. They will be removed in a later release. validate also warns extract-privacy-withheld on a read its component’s own privacy withholds.Go API: ComponentPropertyDef, RequestPropertyDef and MessagePropertyDef each carry one Extract value in place of their per-entity fields.The exported corpus JSON (sightmap export) changes shape to match: component extract becomes an object, and request and message properties carry extract in place of source/field/pattern. This SDK still decodes the old shape, but an older reader cannot decode the new one, so upgrade readers before writers.Carry a node’s interactive state for dom.state (SEP-0013).The probe now captures each control’s current checked, selected, disabled and expanded state from native properties and ARIA, overlaid by the accessibility tree, as "true"/"false" ("mixed" for an indeterminate checkbox). extract: { from: dom.state, path: checked } reads it, including where no accessibility tree is available. It is kept apart from the element’s attributes, so dom.attr and selector matching still see the markup.Add tags to messages, ratify tags on signals, and close the view.tags gap (SEP-0016).tags now reaches every matchable entity. SEP-0004 deferred messages because console and exception events “aren’t a matchable entity in the spec today”; SEP-0006 then created the messages entity, matched by level and a message regex, so that premise no longer holds and no separate mechanism is needed.Message tags resolve as a union across every matching entry, which matters more here than anywhere else: message identity is a refusal rather than a winner, since SEP-0006 requires a consumer to surface an ambiguity when a record matches several entries. The classification survives that ambiguity even though the name does not.signal.tags already shipped via SEP-0007 with a schema description citing a SEP that never mentions signals. It is now documented, and its resolution defined: a signal’s effective tags are its own unioned with the resolved tags of the entity its ref names.Also closes the long-standing view.tags divergence. It was in the JSON Schema but absent from ViewDef and the Go loader’s allowlist, so a corpus carrying a view tag validated under ajv and was rejected by Go. New Corpus.TagsForURL resolves view tags as a union across every matching view, and Corpus.TagsForSignal does the same for signals.Resolve a console record’s tags as a union (SEP-0016).MessageMatch now carries each matched message’s tags, and Corpus.TagsForRecord returns the union across every message a record matches, deduplicated and sorted. The union holds even when the record matches several messages and its identity is ambiguous.Show a page’s view tags in observe (SEP-0004).The view header now prints a tags: line: the union of tags across every view whose route matches the page, so a broad tagged view is not shadowed by the narrower view that wins identity.Honor component privacy when resolving properties (SEP-0009).The matcher resolves each node’s effective privacy (nearest enclosing declaration wins, unmask overrides) and withholds any property value read from a block or mask node, judged at the node the value is read from, so a from: component read cannot surface a value out of a blocked descendant. Under mask, component.exists, dom.state, and dom.attr reads of the four interactive-state attributes still resolve. ComponentMatch now carries the effective Privacy and the component’s own Watch, and validate reports an unknown privacy value as invalid-privacy.Remove the dependencies field from views and components (SEP-0001, Rejected).dependencies asked a curator to hand-maintain a dependency graph that static import analysis derives more accurately and for free, and match --path, the reverse-lookup consumer it existed to serve, was never built. No SDK ever read the field.It is gone from $defs.view and $defs.component in the schema, from the loader’s unknown-field allowlist, from the normative spec, and from conformance fixtures 008-dependencies-binding and 108-fmt-dependencies-canonical.Breaking for any corpus that carries the field. Because the schema sets additionalProperties: false, a view or component with dependencies: is now rejected as schema-invalid rather than silently accepted. Delete the key; nothing consumed it.canonical-format.md keeps its sort-and-deduplicate rule for unordered string arrays. dependencies was its only instance, but SEP-0014 relies on the rule for view- and request-level reference lists.Body paths accept backslash escapes, and sightmap.SplitFieldPath is exported.A request or message property’s extract.path into a body is dot-separated. \. now addresses a literal dot inside a JSON key and \\ a literal backslash, so a key named a.b is reachable. A path with no backslash resolves exactly as before.This is a behavior change for a path that already contained a backslash. a\b used to address the key a\b and now fails to parse; a\.b used to mean the key a\ then b, and now means the single key a.b. validate reports a malformed path as request-property-path-invalid rather than letting the property resolve to nothing.SplitFieldPath exposes the same splitting for tools that compile a corpus into another schema.Add URL properties on views and requests (SEP-0008).A :name segment in a view or request route now binds that segment’s percent-decoded value as a property, needing no declaration. An optional properties[] array reads with the SEP-0017 extract object, from: url.query or from: url.path, for naming a query-string value or renaming a bound segment.One grammar covers both entities. Both carry a route, both are matched against a URL, and the value an author wants out of that URL is the same kind of thing in each case. A request property reads url.query/url.path alongside its body and header sources, so one request may carry URL-shaped and payload-shaped properties side by side.This changes what :param produces, never what it matches: route matching still normalizes :param to *, so every existing route matches exactly the URL set it matched before.A declaration is also a retention request. A consumer that scrubs captured URLs should retain the parts its corpus names, since a scrubbed parameter makes the property permanently unresolvable and omission is otherwise silent. Naming a part is not a privacy grant: a consumer must not treat it as authorization to retain a value its own policy would withhold.Removes transform from the proposal, which SEP-0010 deleted from every property type.Extension: address components by their full path, not their leaf name.Component names are unique only within a parent — that scoping is the whole
point of children:. The overlay treated them as globally unique in three
places, and on any real page that silently produced wrong answers:activeComponents()deduplicated the corpus by name, so a production sign-in map of 104 components collapsed to 54. Clicking a text input resolved two levels deep instead of six, because the survivingFormwas a different form.- Parent scoping in
resolveElement()accepted an unrelated namesake’s match as a component’s parent. PATH.propextraction resolved each path segment by name across the whole corpus, so a submit button’sLabel.labelfound a text field’s label, matched nothing inside the button, and dropped the property.
parentChain plus its name).
resolveElement visits components ancestor-first and requires a component’s own
parent to have matched, and each returned match carries its address.A file-root global is still dropped when a matching view declares a component of
the same name at any depth, as Corpus.ComponentsForURL does, so a view-scoped
$ref expansion subsumes its global instead of matching alongside it.Speed up match.Matcher.Match and match.FindAllMatches on large trees and corpora.The matcher tried every query at every node and allocated a map sized to the whole query set per node, so cost grew with nodes × queries. Queries are now indexed by a requirement of their first selector part (id, attr="value", class, attribute name, or tag), so a node only tries queries it could start. Per-node maps and per-child ancestor-chain copies are gone, and class attribute selectors ([class*="..."] and friends) no longer join the class list on every check. Matching against a 503-query corpus over a 10,000-node tree drops from 290 ms and 466 MB to 6.7 ms and 0.14 MB. Results and their order are unchanged.Adds Element.Attr and Element.HasAttr, which resolve an attribute the way selector matching does (id and class fall back to their dedicated fields).Matcher.Conflicts now deduplicates claims by component definition rather
than by name, and sightmap.Conflict carries the claiming Defs alongside
Names.A component name is unique only within its parent, so two different definitions
can share one. Deduplicating by name silently discarded every conflict between
them — on a production sign-in corpus, five nodes were each claimed by two
definitions and none were reported. Snapshots looked clean while first-match-wins
quietly dropped one claimant on every one of them.Names alone cannot identify a claimant in that case, so Conflict.Defs is
index-aligned with Names, and the [Conflicts] report disambiguates repeated
names by selector. A definition offering several alternative selectors still
counts once, and so does a global together with its $ref expansions, which
share the new ComponentDef.Origin.Don’t surface a blocked node’s accessibility value in the rendered tree.The matcher withholds a property read from a block or mask node (SEP-0009), but the renderer then substituted the node’s accessibility value as value, reintroducing the content the matcher had just withheld: a masked input rendered its own value. The fallback is now withheld under block and mask; declared properties the matcher did surface, such as a dom.state read under mask, still render.SEP-0014: a consumer may fall back to a view’s url: host, but only for a corpus that declares no environments and no origins, so a corpus that predates both keeps compiling as it did. Once a corpus declares either, every host comes from origins, and url: still never defines an environment or origin. A page host is a web concept, so a view that runs only in native environments needs none.browser: enable CDP focus emulation (
Emulation.setFocusEmulationEnabled) on each tab so document.hasFocus() is always true regardless of the real OS window focus. Some page widgets gate behavior on page focus — e.g. react-aria date pickers open only on focus while the page holds OS focus — which an agent-, side-panel-, or headless-driven page never has, making them impossible to actuate in-page. Matches Playwright/Puppeteer, which enable the same emulation by default. Best-effort: a transitional target or very old Chrome that rejects the call simply falls back to the prior focus-dependent behavior.Fix sightmap browser start --port 0 / --cdp-port 0 (documented as
“auto-allocate”): port 0 now asks the OS for a free port and records the port
actually bound in .sightmap/.session. Previously 0 resolved to 0, the daemon
bound an OS-chosen port but wrote serverPort: 0, CDP landed on port 1, and
start --detach exited 0 while every client command then refused the session
with “no running session”. start --detach also no longer reports a session
ready until its file records a reachable server port.Overlay: resolve components when hover lands on a wrapper above the component (e.g. a pointer-events:none control).The overlay extension resolved the hovered element bottom-up with closest(). When a control has pointer-events:none and a styling wrapper sits ABOVE the component container (as JetBlue’s checkout form fields do — a .first-name div wraps jb-form-field-container > input), the hit-test lands on that wrapper, which is an ancestor of the component subtree, so closest() walks up and finds nothing — the field appeared uncovered even though the CLI resolver (top-down) matched it. On a resolution miss the overlay now descends into the hovered element to the deepest active-component element whose box contains the pointer, and resolves from there.Fix the extension overlay rendering nothing on hover for any corpus without a components.yaml.The content script used state.globals.length as its “corpus is loaded” signal. But globals is the optional file-root component list and is omitempty on the wire, so a corpus whose components are all view-scoped — the common case, and what sightmap init produces — serves no globals key at all. The gate was therefore permanently false:- Hover was dead.
onMouseMovereturned before resolving, so#__sightmap-overlayinjected fine but never got any children: no highlight boxes, no tooltip, no console error. Clicks kept working and logging correct component paths, because the click path gates onstate.version— which made the failure look like a hover/render bug rather than a readiness-gate bug. - The corpus was re-fetched on every poll.
pollVersiontook the same gate, so it never reached the cheap/sightmap/versionpath and re-downloaded the whole corpus every 4s, logging[sightmap] loaded N componentseach time.
corpusLoaded() helper keyed on state.version, which fetchSightmap only records once a load returned a non-empty component set — so the “server answered before its corpus finished loading” retry behavior is unchanged.The extension manifest version is bumped so an already-installed ~/.sightmap/extension is re-extracted on the next browser start (ensureExtension only reinstalls on a version change).Snapshot tree hygiene: exclude non-content nodes from the extracted component tree (snapshot --tree-out, capture, --json).The tree builder now prunes two classes of node — together with their whole subtrees — before they reach output, so a captured *.snap.tree.json is app content only and is safe to use as an authoring/golden fixture without post-processing:- The CLI’s own injected overlay (
#__sightmap-overlay,#__sightmap-tooltip). It was visible and non-ignored, so it leaked into the tree JSON and enumerated as bogus candidates. - Document-metadata and non-rendered tags:
<head>,<script>,<style>,<meta>,<link>,<title>,<noscript>,<template>,<base>.
Add the
raw_text extract mode (SEP-0013, first half).extract: raw_text returns a node’s own literal text — the concatenation of its direct text-node children, whitespace-normalized — never its accessibility name. It is pinned to textContent-style data, not the layout-dependent innerText, and it excludes both descendant element text (no <style>/<script> bleed, no subtree concatenation) and CSS pseudo content (::before/::after).It is the deterministic escape for when the accessible name welds in extra text: a heading whose AX name appends a CSS ::after badge resolves text to “Main Most popular” but raw_text to “Main”, so an exact property match (SubFare[tier="Main"]) works. text stays the default (and the right choice when the accessible name is the value); an empty raw_text omits the property, like the other extract forms.(The other half of SEP-0013 — pinning text to a faithful, cross-consumer accessible-name computation, and carrying interactive-state attributes — is deferred.)Add the SEP-0007 Component/View state-signal subset. A corpus-root signals: block names a component (present) or a view (route-active) as a named boolean predicate ({name, ref, tags?}) — the dependency-free core of SEP-0007. Resolve one with Corpus.ResolveSignal / SignalByName; validation reports signal-ref-unresolved and signal-ref-ambiguous. Request/message refs and the temporal/window machinery are intentionally out of scope.Offline matcher: support :not(:has(...)) and a combinator inside :not(...), for parity with the live DOM.The offline component-tree matcher (used by snapshot, coverage, validate, sel-check, and the overlay) now honors two standard-CSS relational selectors it previously mishandled::not(:has(...))and:is(...:has(...))— a relational pseudo nested inside:not()/:is()is now evaluated with the subject’s subtree instead of being silently dropped. This unblocks the leaf-scoping patternX:not(:has(X))(e.g.jb-form-field-container:not(:has(jb-form-field-container))), which previously matched zero nodes offline while resolving live.- A descendant/child combinator inside
:not()(e.g.button:not(nav button)) now parses and is evaluated against the subject’s ancestor chain. Such selectors previously threw a parse error, so a corpus using valid CSS failed tovalidateeven though it resolved live.
:not() now accepts a complex-selector list, matching CSS Selectors Level 4. Sibling combinators (+, ~) remain unsupported and are still rejected loudly at parse time.Note: SelectorPart.Not changes from *SelectorPart to []ParsedSelector to represent the complex-selector-list argument.Reframe the sightmap-authoring skill’s “Property extraction principles” around
purpose: a property earns its place only as a discriminator (makes an
otherwise-ambiguous instance addressable) or as signal (a value a downstream
event/agent needs). A property that does neither — most commonly a text/label
that just restates a node’s accessible name, or a text on a nameless container
that dumps its whole subtree — is noise. Rule 1 now requires a link/button to be
identifiable (by its accessible name, or a useful property) rather than
mandating a property (which invited restating-the-name busywork); Rule 2’s
per-instance discriminator is preserved, with an explicit carve-out for responsive
duplicates (narrow the selector, don’t invent a discriminator). Validated against
the JetBlue corpus (drove ~90% of property decisions cleanly; 21 noise properties
removed with zero tool breakage).Live shadow-piercing query (sel-probe, click/fill, property extraction) no longer under-reports on pages with a form.__smDeepQueryAll walked the DOM via instance properties (node.children, el.matches, el.shadowRoot). On an HTMLFormElement, a control whose name/id is children makes the form’s named getter return that control instead of the element HTMLCollection ([LegacyOverrideBuiltIns]), so for (const c of node.children) threw “children is not iterable” on that form. The blanket catch then returned a truncated result — silently 0 matches for any element after the form in document order (observed on jetblue.com: [data-fs-element="submit"], a.rounded-button, [data-fs-element="points-toggle"], and others reported 0 live while querySelectorAll and the offline matcher saw them).The traversal now goes through cached Node.prototype/Element.prototype accessors, which bypass any named-property shadowing, so form-heavy pages resolve correctly. Affects every live lookup: sel-probe, component-query click/fill/hover, and live property extraction.browser mcp no longer points at Chrome flags that do not exist. The “no WebMCP
tools here” error used to suggest --enable-blink-features=ModelContext,ModelContextTesting
and --enable-features=DevToolsWebMCPSupport; Chrome silently ignores those
names, so following them left document.modelContext undefined on Google Chrome
and made the native path look unshipped. The message and the browser mcp docs
now name the real switch, --enable-features=WebMCPTesting (the same feature as
chrome://flags/#enable-webmcp-testing). Fixes #413.Fix
browser wait-for --component 'X' timing out whenever the query matched more than one node on the page. The wait polled with the strict single-node resolver (the one click/fill/hover need), so a “several match” result was reported as an ambiguity error and mistaken for “not present yet” — even though snapshot listed every instance and wait-for --selector with the same multi-match CSS returned immediately. wait-for --component now treats presence of ≥1 match as satisfied (multiple matches are proof the component is on the page, which is exactly what the caller is waiting for), via a new compquery.Present presence predicate. The interaction commands are unchanged: they still resolve to a single target through the strict compquery.Resolve. Narrowed queries keep working — a property predicate (X[status="Published"]) or an occurrence index (X#0) still matches, and X#N waits until occurrence N exists.Fix
browser mcp call failing against native Chrome WebMCP. executeTool on a native document.modelContext requires the arguments as a JSON string (and returns the result as a JSON string), whereas a JS polyfill takes and returns objects. mcp call passed arguments as an object unconditionally, so it worked against a polyfill but failed on native with “Failed to parse input arguments”. It now detects the native surface (the built-in executeTool reads as [native code]) and serializes the arguments accordingly; the native JSON-string result was already normalized.Add
browser mcp list and browser mcp call for the WebMCP tools a page exposes via document.modelContext. mcp list enumerates the tools (name, description, input schema; --json for full schemas) and distinguishes native, polyfilled, and absent WebMCP — failing loudly with the Chrome flags that enable native WebMCP when there is none. mcp call <tool> resolves a tool by name and invokes it via executeTool; arguments are supplied as a JSON object (--args) and/or repeatable --param key=value pairs. It unwraps the standard CallToolResult envelope (rendering the tool’s text/structured content and any guidance as itself rather than a stringified blob), exits non-zero when the tool reports isError so it is scriptable, and reports not-found or thrown errors distinctly. One tool runs at a single point in time; there is no cross-navigation tool response.Fix browser start silently running sessionless when it couldn’t persist the session file. start now creates the corpus dir if absent (so the session lives at the per-corpus .sightmap/.session rather than a shared $TMPDIR fallback) and fails loudly if it still can’t write it, instead of continuing without a session. Client commands that find no session file now warn before falling back to the default CDP port — where a session may belong to a different corpus or agent — rather than silently attaching to a foreign tab. Applies to both the launched and --attach start paths.Fix browser inject --persist not re-injecting across navigations. The persisted script was registered with Page.addScriptToEvaluateOnNewDocument on the daemon’s collector connection, but that connection never enabled the Page domain — so the command returned an identifier (and inject --list showed the entry) while the script was never actually evaluated on new documents. Only the initial one-shot run fired. The collector now enables Page before registering, so a persisted script runs at document start on every subsequent navigation and new tab, as documented.sightmap lint now auto-reconciles the multi-instance-no-property rule against captured snapshots by default. The static heuristic guesses from selector shape alone and false-positived on container-ish selectors that in fact match a single node; when captures exist under .sightmap/snapshots/, lint now uses their real match counts (a count of 1 suppresses the warning) without needing an explicit --all-snapshots. With no captures present it runs the static heuristics unchanged, and --snapshot/--all-snapshots still control the set explicitly. A missing snapshots/ directory is no longer an error.Add
browser inject for persistent script injection. Unlike eval (which runs once in the current document), inject --persist registers a script with the running session daemon, which re-applies it at the start of every new document in every tab via CDP Page.addScriptToEvaluateOnNewDocument — so it survives navigations and new tabs for the life of the session. --file loads the source from disk, and --list / --remove ID manage the persisted set. Useful for polyfills, overlays, and debugging/experimentation bundles that must outlive a multi-page flow.Docs: sync the authoring and browser agent skills to recent offline-extraction and eval changes. The authoring skill now states that every attribute (not just class/id) matches offline and that text falls back to a node’s rendered innerText when it has no accessible name; the browser skill notes that browser eval awaits a returned promise.The offline element model now captures every attribute the live DOM carries (minus injected sightmap ids and framework-internal
_-scoping attributes like Angular’s _nghost/_ngcontent), not just the curated subset that landed in the generated selector string. Offline attribute selectors ([attr=…], [attr*=…], …) and extract: attr=NAME now match the live DOM for non-standard attributes — e.g. value on a role="option" <li> — closing a source of live/offline divergence.Offline text extraction now falls back to a node’s rendered text when it has no accessible name, so extract: text on role-less elements (custom elements, bare <span>s) resolves the visible value instead of silently empty. Component text is captured from innerText — rendered, transform- and visibility-aware, excluding <style>/<script> and non-rendered nodes — and normalized to a single clean shape (whitespace runs collapsed, ends trimmed) at the build boundary, so snapshot/coverage/bounds and library consumers all see the same value the runtime does. Adds a text field to the component-node schema.browser eval now awaits a returned Promise and yields its settled value instead of an opaque {}, so async page/tool code (e.g. an awaited executeTool()) can be driven directly. A per-eval timeout bounds a never-settling promise. This is a no-op for synchronous expressions.Component properties now resolve offline over the component tree (SEP-0010).
Matcher.Match populates each ComponentMatch.Properties from the matched tree with no live DOM, so a downstream consumer holding a serialized component tree gets property values directly. The extract grammar is the four tree-closed forms — text, attr=NAME, PATH.prop, exists:PATH — and transform is removed from component properties. sightmap validate now checks component properties[]: duplicate names within a component and unrecognized/removed extract modes are errors.Retire the live-DOM component-property extraction pass and remove transform from request and message properties (SEP-0010). Component property values now come solely from the matcher’s offline, tree-closed resolution — the CDP/JS extraction (observe.ExtractProperties and its properties.js) is gone, and snapshot/bounds/query read the values the matcher already resolved. Request and message properties[] keep source/field/pattern; fold any post-processing into pattern (RE2 + capture groups). ApplyTransform is removed.Port the DevTools extension’s component-property extraction to the SEP-0010 tree-closed model. The embedded overlay (resolver.js and its inlined content.js copy) now resolves the four extract forms — text, attr=NAME, PATH.prop, exists:PATH — over the matched component tree, replacing the removed DOM-shaped modes (inner_text/text_only/inner_html/raw CSS sub-selector) and transforms. Descendant paths resolve a child component’s own extracted property (text is the element’s DOM text content, the extension’s implementation-defined accessible text).Update the sightmap-authoring skill for SEP-0010: component properties[] now document the four tree-closed extract forms (text, attr=NAME, PATH.prop, exists:PATH) and the promote-a-sub-element-to-a-child-component pattern. Removed the DOM-shaped modes (inner_text/text_only/inner_html/raw CSS sub-selectors) and all transform guidance (including the request-properties transform reference).sightmap browser start --detach runs the daemon in the background (in its own session) and returns once it is serving, so scripts and agents no longer hang on the foreground daemon. Unlike nohup start &, the detached daemon survives the launching shell (it setsids into its own session). browser start also now prints that it is a foreground daemon holding the shell, and browser status probes the sightmap HTTP server in addition to Chrome’s CDP — reporting ⚠ degraded when CDP is up but the server has been reaped, instead of a misleading running.browser bounds now accepts the full component-query grammar — property predicates and descendant chains — the same engine as click/fill/hover/wait-for. Previously it matched component names only, so bounds 'Card[title="X"]' or bounds 'Row Star' returned “matched no component”. Multi-match is preserved (a query returns every matching component’s box), --substring still does name-only fuzzy matching, and --all is unchanged. Also documented the existing wait-for --component and --view flags in browser --help.Authoring + browser skills: document the browser daemon lifecycle. browser start is a long-running foreground daemon that holds the shell; scripts and agents should use browser start --detach (which returns once serving and survives the shell) rather than nohup start &. Also note headless auto-detection and the --no-sandbox hint on sandboxed hosts, and that browser status can report a ⚠ degraded daemon.browser start now comes up out of the box on headless and sandboxed Linux hosts. With no display ($DISPLAY/$WAYLAND_DISPLAY unset) it defaults to headless instead of dying with “Missing X server or $DISPLAY”, and when Chrome’s launch fails because the host restricts its sandbox (unprivileged user namespaces clamped by AppArmor or a container) the error points straight at the fix (--chrome-flag=--no-sandbox) rather than leaving you to decode Chrome’s stderr.Authoring skill: document the component model. Add “Component hierarchy (
children:)” and “Cross-view references ($ref)” sections covering nested components, parent-scoped selectors, the depth budget, and $ref attestation. The browser skill now spells out that component queries use a descendant combinator (no > child combinator) and that a multi-match is an ambiguity error, not a missing component.Authoring skill: fix the “Further reading” pointers. They referenced go/README.md (not shipped with the installed skill) and docs/reference.md (which does not exist). The skill is self-contained — coverage model, outer loop, tool surface, lint rules, quality checklist, and the component model are all in it — and now points to the published docs for the normative spec instead of dead repo-relative paths.Authoring skill: fill three small content gaps. Document the memory: shape (a list of plain strings, attachable at the file root or on a view/component), add a route-matching note (* matches one path segment, ** matches zero or more, and the URL is matched decoded so %2F counts as a separator), and reconcile the class-selector guidance (stable hand-authored class names are fine — the volatility risk is generated/hashed classes).sightmap multi-coverage no longer manufactures phantom “global candidate” promotions from stale capture directories. It grouped columns purely by directory name under snapshots/, so a leftover or renamed dir (e.g. an old snapshots/views/ kept alongside the current snapshots/home/ for the same page) became a second column and made that page’s own components look like they “appear in 2+ views” — advising the author to wrongly globalize view-scoped components.A capture dir is now treated as a real view only when it matches a view in the current corpus (by SnapBasename). Non-current dirs are still shown in the matrix (marked *) for context but are excluded from the cross-view global-candidate analysis, and a warning names them so the author can delete the stale dir or re-capture under the current name. The sightmap-authoring skill documents the new behavior.Two authoring-clarity fixes for the offline inventory and the capture gate.sightmap stats now attributes corpus-root global components and requests to their own (global) · (all views) row instead of showing 0 against every view. A corpus whose coverage lives entirely in globals (e.g. a single-view app mapped with global components) previously rendered a per-view table that read as empty, with the confusing footer “per-view rows sum to 0”. The globals are shown once, on a leading row, and the footer explains them — globals apply to every view, so folding them into one view’s row would misattribute coverage. The --json contract is unchanged.sightmap capture’s novelty-gate message no longer reads like it is refusing a first baseline. The first capture of a view always writes (an empty set can’t be redundant); reaching the gate means a baseline already exists and the new capture adds nothing, so the message now says so plainly (“<view> already has N capture(s); this one adds no new component or interactive slot — not saved”). The sightmap-authoring skill is updated to match and to reinforce the first-capture-always-writes guarantee.browser install now fetches the correct architecture on linux-arm64. It previously mapped every Linux host to the linux64 (x86-64) Chrome for Testing build regardless of CPU, so arm64 machines silently got an x64 Chrome under ~/.sightmap/browsers/chrome-<ver>/chrome-linux64/. The platform is now resolved per-arch (linux-arm64 on arm64, linux64 on x64). Because Google does not currently ship an arm64 build to the Stable channel, resolution falls back through Beta → Dev → Canary for platforms Stable does not carry, printing a one-line warning when it uses a non-Stable channel. Every mainstream platform continues to install from Stable unchanged.Authoring efficacy on hook-poor DOMs (Salesforce Lightning, Angular, framework-generated markup):
- Generalized selector-candidate generation.
gapandsuggestno longer dead-end when a DOM has nodata-testid/data-component. Candidate generation now ranks custom-element tags, design-system classes, ids, other stabledata-*, andname/href/ariahooks (data-attributes remain the top-ranked input, not an override), dropping only clearly machine-generated tokens.gapalso emits a container hook on the hook-poor path. - New
explaincommand. Node-first inspection: pick nodes by selector,--id, or--grep(role/name) — live or offline via--snap— and dump each node’s facts, ranked selector candidates, coverage tier + owning component, and ancestor hooks. Shadow-transparent (matches the offline matcher), so authors no longer hand-read*.snap.tree.json. - Honest coverage.
snapshot --coverageandgapnow warn when a clean “0 orphaned” pass is carried entirely by global (chrome) components — i.e. the current view modeled nothing and the pass reflects a global backstop rather than a real view. - Authoring skill: a second mandatory property rule requires a per-instance discriminator on any component whose selector matches more than one instance, so repeated cards/rows/tabs stay individually addressable; plus
explaindocumentation.
/sightmap fetch through the background service worker, and on a cold/just-woken MV3 worker that round-trip intermittently delivered an empty corpus at page load; the empty result then stuck for the life of the page because the per-instance version never changes (so pollVersion never refetched) and an open side panel keeps the worker alive. sightmap snapshot was unaffected (it reads the corpus in-process over CDP). The content script now fetches the local server directly on http-origin pages — host_permissions already grants http://localhost:* and the server sends Access-Control-Allow-Origin: * — using the background proxy only as an https mixed-content fallback, and it treats an empty corpus as “not loaded yet” and keeps retrying instead of caching it.Fix snapshot/capture rendering an empty component tree when the page root (or any ancestor) is reported invisible. render.Filter dropped an entire subtree at the first IsVisible=false node, but a zero-size or visibility:hidden ancestor — notably the document <html> root, which reports height 0 / ignored — can have fully visible descendants, so the whole annotated tree vanished even though coverage still counted the visible nodes. Invisible nodes are now made transparent (the node itself is omitted, visible descendants survive); a genuinely hidden display:none subtree still renders nothing, keeping render in lockstep with coverage.sightmap stats now reports a Messages count. The offline inventory listed views, components, requests, properties, and memory but silently omitted the SEP-0006 messages: entity, so a corpus’s console/exception matchers were invisible in the totals (and in --json). Adds messages to the totals table and the --json contract.browser start --attach host:port attaches the daemon (devtools server +
console/network collector) to an already-running Chrome’s CDP endpoint instead
of launching and owning its own. It is a deliberately degraded mode: no owned
profile or extension guarantees, capture is complete only from attach onward
(pre-attach network/console history can’t be recovered — Network.enable
replays nothing), and browser stop detaches rather than killing the
caller-owned browser. The collector and devtools query surface are unchanged —
the collector only ever needed a CDP address — so live console/exception and
request matching work identically once attached.Also fixes a latent shutdown-order bug in the collector surfaced by attach mode:
Collector.Stop cancelled the per-tab drain contexts after wg.Wait(), which
deadlocked whenever the CDP connections were still healthy at stop time (the norm
when a consumer stops the collector without tearing down the browser). Stop now
cancels the drains first and refuses new tab attachments during teardown.And fixes extension-server-port injection under attach: findExtensionSWWS took
the first extension service worker it found, so in a real browser with several
extensions installed (the attach case) it injected the port hint into the wrong
one and the sightmap overlay never learned the server port — falling back to
probing 7891–7900 and latching onto whatever sightmap server answered first
(often a different session). It now identifies the sightmap extension by its
manifest name before injecting, so the correct overlay gets the hint. An owned
launch never hit this: its isolated profile contains only the sightmap extension.The browser start devtools now surface extracted property values, not just
matched-def names. The collector eagerly retains request/response bodies for
XHR/Fetch traffic (request bodies inline from requestWillBeSent; response
bodies fetched on Network.loadingFinished, off the event loop), so a buffered
Request record is complete. sightmap network list|get annotate via
RequestsForRecord — resolving each matched request’s properties[] against the
live headers/body — and sightmap console list|get surface a matched message’s
stack properties[]. Extracted values render as a trailing {name=value, …}
token on list lines and a Properties: block in get detail, e.g.
GET /api/checkout/pay → 200 OK (Fetch) {outcome=declined} — the “200 OK but the
body says declined” case, now visible.Make selectors behave consistently across shadow DOM. The captured component tree already flattens shadow roots (so offline matching, sel-probe’s offline count, and coverage pierce shadow), but every live-DOM operation re-found nodes with document.querySelector, which cannot cross shadow boundaries — so property extraction, interaction (click/fill/hover/value/scroll/wait-for), bounds, sel-probe’s live count, and suggest/discover were silently shadow-blind and disagreed with the corpus. A shared shadow-piercing resolver (browser.DeepQueryJS) now backs all of them, so live operations reach shadow-DOM content the same way matching does (property extraction on shadow-DOM leaves, interaction with shadow-DOM controls, and discovery of shadow-DOM links now work). spec/v1/schema.md formalizes the selector/tree model and its shadow-DOM semantics — a deliberate divergence from live document.querySelector, which does not cross shadow roots.Expand the authoring and browser skills to cover the runtime spec additions.- sightmap-authoring: new “Requests and messages” section documenting
requests:(route/method), requestproperties:extraction (SEP-0005source/field/pattern/transform), andmessages:(SEP-0006level/message+source: stackstack-addressing properties); a new “Runtime activity” step in the per-page loop plus a runtime line in the “Done when” checklist so the loop routes authors to these entities; and view-route-generality guidance (a catch-all route that matches every page is a smell — carve specific routes). - sightmap-browser: reframe the
console/networktooling as the runtime view of the corpus — each record leads with a[Match]/[--]slot and trails extracted{name=value}properties — with the observe-only “reproduce the traffic” note and--url-substring / SPAwait-forcaveats. - Fix stale command references:
sel-probe -- 'selector'(the baresel-probe 'selector'form fails) andgap --include-hidden(the documented--visibleflag does not exist).
Request records carry
request/response headers (from the CDP requestWillBeSent/responseReceived
events — cheap, no extra round-trip) and an observed DurationMs; observed
exception Message records carry the Stack (from exceptionThrown
stackTrace.callFrames, throwing frame first, with 0-based line/column
preserved). Bodies remain fetched on demand. This is what lets
RequestsForRecord extract body/header properties and MessagesForRecord
resolve stack properties against traffic the reference tooling captured.Make network get reliable and header-aware. The devtools body endpoint now serves the response/request body eagerly retained on the record (captured at loadingFinished) instead of always re-fetching it from Chrome via getResponseBody, which failed once the browser had evicted the body (“response body no longer available”). And network get now renders the captured request/response headers (they were already on the record from the collector but never surfaced). This closes the HTTP response header/body residual split out of the network-collector gaps.match.Matcher gains a chain-matching entry point for consumers that classify a
stream of individual observed elements rather than a full component tree. Each
observed element carries only its own root→leaf ancestor chain, so there was no
first-class API for it — a consumer had to reimplement the spec’s component
identity/tag resolution by hand (and route-blind). MatchChain(chain, pageURL)
runs the shared NFA matcher over a single-branch spine built from the chain and
returns depth-annotated ChainMatch values; NamesForChain applies the
nearest-enclosing identity rule and TagsForChain the tag-union rule (deduped,
sorted), so the spec’s resolution lives once in the shared library. Matching is
route-aware, so view-scoped components apply on the chain exactly as in a
full-tree match. Purely additive packaging over already-exported pieces — no new
matching semantics.messages: can now match and extract on exception stack traces (a SEP-0006
follow-on). An observed Message gains an optional Stack []Frame (throwing
frame first; Frame{Function, File, Line, Column}), empty for a plain console
record. A MessageDef gains a stack-addressing properties[] mirroring
SEP-0005’s source/field/pattern/transform: the one source is stack,
field names a frame and attribute (top.file, top.function, 1.line, where
top aliases frame 0), an optional RE2 pattern refines the resolved value,
and transform post-processes it. Corpus.MessagesForRecord now folds the
extracted values into each MessageMatch.Properties, bringing it to parity with
ComponentMatch/RequestMatch; unresolved values are omitted silently, so a
plain console record (no stack) simply extracts nothing. The reference CLI
validates the new declarations (message-property-invalid-name /
-source-invalid / -no-field / -pattern-invalid).Observed Request records can now carry the payload a RequestDef’s
properties[] addresses, and a new matcher resolves them — the runtime half of
SEP-0005. Request gains optional (omitempty) DurationMs, ReqHeaders /
RspHeaders ([]Header), and ReqBody / RspBody (*Body) fields, so a
producer holding a full capture can populate them while a lazy producer leaves
them nil and the wire stays lean. Corpus.RequestsForRecord(rec) []RequestMatch
does route+method identity matching (via RequestsForURL) and then resolves each
matched def’s properties[] against the record — source → field (JSON
dot-path with numeric array indexing for a *.body; case-insensitive header
lookup for a *.headers) → optional RE2 pattern (capture group 1 else the whole
match, or a scan of the raw source when field is absent) → optional
transform. Unresolved values are omitted silently per the spec, so an
incomplete record degrades to fewer properties rather than erroring.Add
sightmap export [dir]: load a .sightmap/ corpus and emit the canonical
Corpus wire — the exact json.Marshal(sightmap.Corpus) shape a library consumer
reads (selectors[] arrays, components nested under each view, plus requests
and messages) — to stdout, a file (-o FILE), or an HTTP endpoint (--url,
POSTed as application/json with no auth headers). The .sightmap/ directory is
auto-detected by walking up from [dir] or the cwd, and TLS verification is
skipped for local hosts (localhost, 127.0.0.1, .test) or under an explicit
--insecure. A companion sightmap push URL [FILE] POSTs a corpus JSON (from a
file or stdin) through the same transport.This replaces the hand-rolled Python collector (collect_and_upload_sightmap.py)
that shipped a second, lossy serializer — it flattened views into compound
selectors and dropped routes, requests, and messages. Routing the upload through
the Go loader makes it the single source of truth and shares the exact
sightmap.Corpus type with the server-side reader, so the two ends cannot drift.sightmap serve and the browser start daemon now serve the canonical Corpus
wire — the same shape library consumers see (selectors[] arrays, components
nested under each view) — inside a thin {site, version, corpus} envelope,
replacing the bespoke pre-compiled shape. The bundled browser extension consumes
it directly; cache-busting via /sightmap/version is unchanged.Restructure the Go library for downstream consumers. All corpus vocabulary,
observed runtime records, and match-result types now live in a single
self-contained
sightmap package, with the matching engines consolidated behind
one match.Matcher. Types follow a consistent naming model — …Def for a spec,
bare structs for observed records, …Match for results — and extracted values
share a typed PropertyValue. This is a breaking change to the Go import surface
for library consumers (there were none before this release).Add runtime matching for console/exception messages. Corpus.MessagesForRecord
classifies an observed console record against messages: definitions
(case-insensitive level equality + an RE2 message regex, precompiled at load),
returning every match so an ambiguous record is surfaced rather than silently
resolved. The sightmap console and sightmap network devtools listings now
annotate each captured record with the corpus definitions that classify it —
messages via MessagesForRecord, requests via route+method identity — leading
each line with the match.Add a top-level
messages: entity (SEP-0006): named console-output and exception patterns, matched by level and a message regex. This gives console activity what requests: gives network activity, a named entity the rest of the corpus can reference by name.An uncaught exception arrives as level: exception, not level: error. The reference capture emits log, debug, info, warn, error, and exception, so level: ERROR matches console.error and does not match an exception. SEP-0006 still needs no kind: discriminator, because the origin is carried as a level value, but a corpus that wants exceptions has to name them.New validation:message-regex-invalid(error) compilesmessageat validation time, matching how component selectors are already checked. The corpus no longer stores a pattern nobody has proven is a pattern. The dialect is pinned to RE2 (Goregexp/ there2npm package for JS): a linear-time syntax with no backreferences or lookaround, so authoring-time validation and runtime matching agree across SDKs.merge-collision-message(warning) reports a duplicated name. This one is load-bearing for SEP-0007:ref:resolution counts distinct entity kinds, so two messages sharing a name collapse to one kind and the ambiguity check never fires.message-conflict(warning) reports two entries that can match the same record, where that overlap is statically decidable: samelevel, identical or absentmessage.message-level-unknown(lint) catches a level outside the emitted vocabulary. The realistic trap isWARNING, CDP’s own spelling, which the capture normalizes towarn.field-type-invalid(error) now covers message fields, somessage: 404andlevel: 500are rejected in Go as ajv already rejected them.
properties: to request definitions (SEP-0005): named values a consumer extracts from a live request/response pair, so a 200 OK whose body says a payment was declined can be reasoned about.sourcenames the root (a closed enum:req.body/rsp.body/req.headers/rsp.headers).fieldselects a value withinsource: an object-key dot-path for a body source, a header name for a headers source (required there).patternis an RE2 regex (Goregexp/ there2npm package for JS; no backreferences or lookaround) that refines whatfieldresolved, or scans the raw source text whenfieldis absent.fieldandpatterncompose (anyOf) instead of being mutually exclusive.transformshares the component-property vocabulary (unchanged; cleanup tracked separately).
request-property-invalid-name,request-property-no-extractor,request-property-source-invalid,request-property-headers-require-field, andrequest-property-pattern-invalid(errors) enforce in Go what only ajv enforced before.request-property-shadows-reserved(warning) fires when a property is namedstatus,method, orduration, which shadows the request’s HTTP identity and makes it unreachable from a signal filter.field-type-invalid(error) rejects an unquoted non-string scalar in a schema-string field. yaml.v3 decodes any scalar into a Go string by taking the raw lexeme, sosource: 200used to load as"200"while ajv rejected it.
source/field/pattern and applies no transform. spec/v1/schema.md marks the evaluation requirements as such, and the 018-request-properties conformance fixture is now executed by the Go test suite.Add
sightmap atlas: find searches the community atlas at sightmap.org/atlas, list browses it, and add installs one corpus into .sightmap/.find matches slug, name, domains, categories, and description, ranked so an exact domain match comes first, because an agent about to automate a site starts from a URL and has no way to guess a slug. Each hit prints its own sightmap atlas add command, and a query that matches nothing exits 0.add fetches one .tar.gz and never reads the index, so an index outage cannot stop an install. It refuses a non-empty target before touching the network; there is no --force. --index and --source point the verbs at a mirror or a private corpus store, under the same HTTPS-only transport policy and the same archive caps.New public API: sightmap.Totals, the five corpus-wide counts split out of sightmap.Stats, which now embeds it. Field access and the sightmap stats --json output are unchanged — the JSON is byte-identical — but a composite literal must now write Stats{Totals: Totals{Views: 1}}. The split exists because a published catalog entry carries these five numbers as its stats object and puts the per-view rows in a sibling field, so atlas.Entry.Stats is a sightmap.Totals: one definition of what the counts mean, and no PerView field that no valid catalog could ever fill. As a side effect sightmap atlas find --json now prints a count that is zero rather than omitting it, and reports properties and memory, which it previously discarded.Add corpus statistics, in the library and on the CLI.New public API: sightmap.Stats, sightmap.ViewStats, and (*Corpus).Stats(), so any consumer of a loaded corpus — the atlas index generator, Subtext — gets the counts without shelling out to the CLI. Components counts distinct component names corpus-wide (a global reused by three views is one component), while Properties and Memory are summed over distinct component definitions, so a $ref-expanded copy counts once but two views that each define a different component under the same local name both count. Stats.IsEmpty reports a corpus with nothing in it — memory alone is not nothing.New sightmap stats verb over that API: the totals plus a per-view component/request table, and --json for a stable machine-readable form (views / components / requests / properties / memory / per_view) suitable for CI consumers. stats refuses a corpus that sightmap validate rejects, since the loader drops the definitions it cannot resolve and the counts would silently under-report; in --json mode the failure is itself JSON (an error key plus diagnostics), so a consumer always has something to parse.Breaking: rename the rule-side corpus types to the
Def convention — match.SightmapComponent → match.ComponentDef and match.SightmapMatch → match.ComponentMatch. A ComponentDef describes what to match; a ComponentMatch is the result of matching one. No behaviour change; callers update to the new names.Add sightmap init: scaffold a schema-correct .sightmap/ corpus (a commented components.yaml and views/example.yaml carrying version: 1 and the top-level views: wrapper) so the first files an author sees are already valid instead of written from memory. Existing files are never overwritten, and the scaffolded corpus passes sightmap validate as-is.Breaking: split the matching engine out of Corpus. Corpus is now pure, serializable data; build a Matcher with NewMatcher(corpus) to match a live component tree. Corpus.MatchTree and Corpus.Components move to Matcher.MatchTree / Matcher.Components (the per-URL compiled-query cache now lives on the Matcher). Read-only corpus queries (ComponentsForURL, ViewForURL, RequestsForURL, GlobalComponentNames) stay on Corpus.Model requests: in the corpus and make it serializable to a stable wire form:- Global and view-scoped API request definitions (
RequestDef/Payload/Field) are now parsed into the corpus — previouslyrequests:was known to the schema but silently dropped. - New
Corpus.RequestsForURL(url, method)returns every request whose route glob (and optional method) matches the observed request — “all matches apply”, per the spec. Reuses the existing route matcher (:param, trailing-slash,**). Corpus,View, andRequestDefnow carry JSON tags, so a corpus serializes to a lean wire form (memory/globals/views/requests); authoring-onlyViewfields (url/access/snapshots/sourceFile/stability) are excluded. The flattened list is emitted in a stable pre-order (lexical file order, then declaration-order depth-first), locked by a test, so the wire form is reproducible.
- Subcommand
--help/-hnow exits 0 and no longer printsflag: help requested— a help request is a success, not an error (#117). browser statuson a session file that exists but doesn’t parse (e.g. hand-written or corrupted, so no validport) now reports it as an unrecognized format with the expected shape, instead of a misleading “server and CDP were assigned the same port (0)” collision hint. The collision hint is also gated on a real server port (#118).validateno longer warns about unknown fields in tooling files it owns (survey.yaml), as it already skipsconfig.yaml(#119).
browser start work — and fail legibly — on Linux and in root containers (the standard agent/CI environment):FindChromenow checks the sightmap-managed Chrome for Testing install (~/.sightmap/browsers/) on Linux and Windows, not just macOS, so the documentedbrowser install→browser startflow works. The “no Chrome found” errors now point atbrowser install.browser startadds--no-sandboxautomatically when running as root (Chrome refuses to start otherwise), and accepts--chrome-flag(repeatable) and--chrome-binaryto override the launch.- On a startup timeout,
browser startnow reports the resolved binary, the full argument list, and the tail of Chrome’s stderr — instead of a bare “timed out waiting for CDP” that hid the real cause.
validatenow errors on an unsupportedversion:value (e.g.version: 2) — the spec requiresversion: 1. This is the companion to the missing-version:warning.browser install --helpprints usage and exits 0 instead of ignoring its arguments and starting the 184 MB Chrome-for-Testing download (the subcommand now parses its flags).searchnames the near-miss when a pattern matches a view or request name (search covers component fields only), instead of a bare “no matches”.
** is now a proper globstar. As a whole path segment it matches zero or more segments, so /admin/** matches /admin itself (as the spec always stated) as well as /admin/users and deeper, and /a/**/b matches /a/b. A ** glued into a segment (e.g. /foo**) is treated as a regular single-segment * that does not cross a /. This fixes the previous behaviour where /admin/** failed to match /admin and /messages** failed to match /messages.Remove the authoring skill’s instruction to run sightmap browser register --addr localhost:PORT — that subcommand does not exist, so agents following it dead-ended. Attaching to an externally-launched browser remains a possible future feature (tracked upstream); until it lands the skill no longer documents it.report and capture errors now teach the views: file structure instead of naming a field in isolation:reportdistinguishes “no views defined at all” from “views exist but none has aurl:”, and both print a minimalviews:example (withroute:andurl:and their roles) — previously it always said “no views with URLs found / Add a url: field”, which contradictedcapture’s route-only advice.capture’s “no view matches” error shows the sameviews:example and namesroute:explicitly.
sightmap-authoring skill’s Phase 1a now shows a complete view-file example (the top-level views: list with version:/route:/url:), instead of only telling authors to “create a view file with route:” — which invited putting view fields at the file root (silently making it a globals file).Teach the corpus schema at the point authors get it wrong, instead of failing silently or generically:- A view field at the file root (e.g. a top-level
route:/name:) now warns that view fields belong under a top-levelviews:list, with a short example, instead of a bare “unknown field”. - A view-shaped file that sets
url:andcomponents:but noviews:now warns that it defines no views and its components are treated as global (previously it validated clean and silently became a globals file). - A missing
version:now warns (the spec requiresversion: 1in every corpus file). validatenow warns when the whole corpus has global components but no views — it can never match a view, so capture, report, and per-view coverage are unavailable.
Go library: components can now carry
source: (relative path to the implementing file — already schema-recognized, previously dropped by the loader) and tags: (authored classification labels, e.g. defect). Neither is inherited by children, matching memory/properties/stability’s existing convention. Tags also flows through ApplySightmap’s match result alongside Memory. New Corpus.AllComponents() returns every component in the corpus (globals plus every view’s), deduped by first-seen name — the flat, whole-corpus list a consumer building an upload payload or a lint/coverage report wants, replacing an equivalent hand-rolled loop in cmd_lint.go.The release workflow now pushes a
go/vX.Y.Z tag alongside the existing vX.Y.Z tag. This module lives in the repo’s go/ subdirectory rather than at the root, and Go’s module versioning requires a nested module’s tags to be prefixed with that subdirectory — so go get github.com/sightmap/sightmap/go@vX.Y.Z has never actually resolved to a tagged release, only to @latest’s branch-tip pseudo-version. The bare tag is unchanged and still drives everything else (goreleaser, npm publishing, the release-already-tagged check).Go library:
Corpus.Memory now carries file-level memory entries (the loader previously dropped them). Lower the module’s go directive from 1.25.2 to 1.23, its actual dependency floor, so consumers aren’t forced onto a newer toolchain than the code requires.Verify the changesets release automation end-to-end: no functional change.
Offline selector matching now matches the live DOM for
id, class, and SVG. Three gaps are closed so a selector sel-probe verifies live behaves the same way snapshot/coverage/capture see it offline:- Attribute selectors on
id([id^="issue_"],[id$=…], …) now match.idlives in a dedicated node field, so the matcher resolvesid/classattribute selectors to those fields — not only to capturedattrs. placeholderis captured, soinput[placeholder="…"]matches offline.- SVG classes are captured. On SVG elements
classNameis anSVGAnimatedString, which broke the old extraction (it threw and dropped the whole selector);probe.jsnow usesclassList, sosvg.lucide,[class*="lucide"], and:has(svg.lucide-x)match offline.
class and id are no longer described as “don’t work offline” — they work, for HTML and SVG alike.Better handling of asynchronous SPA navigation, without baking implicit waits into click.navigatenow reports client-side redirects. It previously only saw server-side (HTTP) redirects, which are reflected by the load event; a client-side redirect that fires during hydration (an auth guard bouncing/login → /, or/ → /workspace) went unreported, so the caller was told it was somewhere it wasn’t.navigatenow waits briefly after load for a follow-up navigation and prints(redirected to FINAL)for those too.wait-forgains--viewand--component. These are the explicit, corpus-aware step boundaries to use after an action that should navigate (the act-then-wait split Playwright and Selenium use):--view <Name>waits until the current URL resolves to a named sightmap view, and--component '<Query>'waits until a component query — including property filters likeWorkItemRow[key="FALCON-7"]— matches a node. Both auto-retry until they hold or time out loudly.--url,--selector, and--loadremain the raw equivalents.
click deliberately does not wait for or guess about resulting navigation — it acts, reports, and keeps its loud covered/off-screen refusals. Adds browser.AwaitNavigation (waits for Page.frameNavigated / Page.navigatedWithinDocument, settling chained redirects) behind navigate.Coverage and the annotated tree now agree on what counts as visible.
probe.js set each node’s isVisible from that element’s own computed style, which misses ancestor-driven hiding: a control inside a closed opacity:0 overlay (a dismissed dropdown or context menu) has computed opacity:1 of its own, so it reported visible even though it is painted nowhere. Coverage counted these while the renderer dropped the hidden container’s subtree, so on real apps a large share of a page’s interactive nodes (all the closed menus) silently inflated the count and tanked the score.probe.js now computes isVisible with the browser’s own Element.checkVisibility, which accounts for ancestors hidden by display:none, visibility:hidden, opacity:0, or content-visibility — keeping the layout/rendering judgment in the real browser instead of re-deriving CSS semantics. snapshot/coverage “(visible only)” now excludes descendants of hidden containers; --include-hidden still counts every interactive node. The renderer also drops invisible subtrees regardless of interactivity so it and coverage read one signal.snapshot now surfaces runtime match conflicts in a [Conflicts] section — the ambiguities that only exist against a live page, complementing the static corpus-conflict warnings validate emits. Two triggers:- A single DOM node matched by more than one distinct component name. Matching is first-match-wins, so only one applied and the others were silently dropped — which is also why a correct-looking component can report
0 matches. The section names the node, the competing components, and which one won. - Two or more views matching the current URL at equal specificity, where declaration order alone decided the winner.
match.FindConflicts and Corpus.TiedViews; both are computed during observe.Page. (The view-tie half is provisional pending the route-specificity decision in the functional-decomposition proposal.)validate now surfaces two classes of invalid corpus the loader previously dropped silently (so they shipped with a green check):- Missing required fields — a component with no
selector(or noname), and a view with noname, are now errors (missing-selector/missing-name/missing-route) instead of being quietly discarded during load. - Unresolved
$ref— a$refnaming a component that no file defines is now aref-unresolvederror (matching the spec’s MUST), instead of the reference being silently skipped.
validate now warns on unknown fields. The typed loader silently ignored any YAML key it didn’t recognize, so a typo like memroy: — or a half-baked field — vanished without a trace. validate now walks the raw YAML and emits an unknown-field warning (not an error) for any key the spec doesn’t define at its position, at any nesting depth. It warns rather than rejects, so authors can stash experimental fields (e.g. macros:) during development; recognized fields — including the reserved tooling fields access and snapshots — are never flagged, and .sightmap/config.yaml is excluded. This completes strict validate alongside the earlier required-field and $ref checks.Fix report and make the view url: field first-class. url: is now read per view (with a file-level url: as a default for views that omit their own), instead of only at the file level. Previously per-view url: was silently dropped, so report — which needs a representative URL per view — errored with no views with URLs found even when every view declared one. url: is also now part of the published schema (SEP-accepted alongside properties), so a corpus that declares it validates instead of failing additionalProperties: false.validate now distinguishes errors (which fail validation, exit 1) from warnings (advisory, exit 0), and warns on silent corpus conflicts that a fallback rule was resolving without telling you:merge-collision-view— two or more views share aname.route-conflict— two or more views share the same (normalized)route; only the first-declared applies to that URL (this is the “same-route hijack” that can silently drop a view’s components).merge-collision-component— two or more root-level global components share anamewith different selectors.
code and severity. Scoped component name reuse (the same child under multiple parents) is correctly not flagged, since that is intentional. The spec’s diagnostic-code table documents the new corpus codes.Make snapshot loud about corpus-match state instead of silently omitting it. When a corpus is loaded but no view’s route matches the URL (e.g. an auth redirect to a login page), the output now shows a [No view matched] <url> notice instead of a headerless tree that looks identical to a normal snapshot — so an agent knows the page is off the map. And the [Coverage] summary is now printed whenever a corpus is applied, including for a matched view that has no components yet (every interactive node reads as an orphan); previously that case printed only the view header, leaving the documented snapshot --coverage bootstrap with nothing to iterate on.Make
browser click and fill fail loudly instead of silently no-op’ing. click now scrolls the target to the center of the viewport and verifies its center is the top-most element there before dispatching — it errors when the target can’t be positioned in the viewport (previously an off-screen target below the fold was clicked at a coordinate that hit nothing and still exited 0) or is covered by another element (an open overlay/modal). The success confirmation reports the real post-scroll coordinates. fill now reads the value back after typing and errors when a non-empty value was typed but the field is still empty — the signature of a React-controlled input where plain typing doesn’t stick — telling you to retry with --clear.sel-probe now cross-checks the offline matcher. It queries the live DOM as before, but also runs the same selector through the offline matcher that snapshot/coverage/capture use, prints that count, and emits a ⚠ offline/live divergence warning (with a hint) when the two disagree. This closes the false-confidence trap where a selector “verified” against the live DOM is silently dead in the corpus — e.g. attribute selectors on id ([id^="…"]) match live but never offline, because id is a dedicated node field rather than a matchable attribute.Make browser-use feedback loud: the interaction commands (
click, fill, hover, keypress, scroll, drag, wait-for, dialog, tabs resize) now echo a short confirmation on success instead of exiting silently — e.g. clicked CartNavButton @ (674,30) — so an agent driving the browser can see what happened. Several raw CDP/Go errors are now rewritten into actionable messages: a wait-for timeout reads timed out after 800ms waiting for selector "..." (not context deadline exceeded), resolving a dialog when none is open says so plainly, and fetching an evicted network response body explains that bodies are only retained briefly. snapshot now prints a note when no .sightmap corpus is found at the target directory instead of silently rendering an un-annotated tree.Stop green-lighting broken corpora and blank pages:
snapshotnow exits non-zero when the corpus is present but fails to parse (a missing corpus stays non-fatal — the tree still renders), and when the observed page has 0 interactive nodes (blank or still loading). It still renders whatever it observed first.inspectandsuggestnow warn on a bad corpus instead of silently ignoring it.- Coverage no longer marks a page with 0 interactive nodes as a pass. The coverage line renders a distinct
∅mark (instead of✓), thecoveragecommand counts empty captures as failures, andcapturerefuses to persist a blank/loading page as a view’s baseline (override with--force).
Make corpus loading robust against two malformed inputs that previously failed badly:
- A circular
$refchain (A → B → A, or a component that references itself) sent every corpus-loading command into infinite recursion and hung the process. Loading now detects the cycle, stops expanding, andvalidatereports it as aref-circularerror instead of hanging. splitSelectorsonly balanced parentheses, so a comma inside an attribute selector or quoted string ([data-x="a,b"]) was wrongly treated as a selector-list separator and split into two dead alternatives. Splitting is now aware of[]brackets and quoted strings (with backslash escapes), matching how CSS is actually written.
/*/projects matches the /acme/projects/ paths that Django, Rails, and many React-Router apps emit — previously such URLs matched no view at all, silently dropping the [View:] header, view memory, and view-scoped components. Express-style :param segments in view routes now match a single path segment (and score between a literal and * for specificity). And Corpus.ViewForURL now returns the most specific matching view — using declaration order only to break equal-specificity ties — instead of the first match in corpus order, closing a divergence between the exported library and the spec.Isolate concurrent browser sessions so agents working in different projects no longer cross-talk. The session file is now keyed to
--sightmap-dir (it lives at <sightmap-dir>/.session) instead of a single shared $TMPDIR file, so a second browser start only ever reuses the Chrome for its own corpus rather than piggybacking on another agent’s browser. Free-port probing now checks the IPv4 loopback (where Chrome’s CDP and the sightmap server bind), and the sightmap server binds 127.0.0.1, so two daemons started on the default ports slide to non-overlapping ports instead of one daemon’s server colliding with another’s CDP port. Every session-aware browser command (including console/network, status/stop, tabs, and the low-level interactions) now accepts --sightmap-dir to select which session it talks to.Fix the Sightmap overlay getting stuck in a reload loop that flooded the captured console. The extension’s version poll compared the raw /sightmap/version JSON text against the parsed version string, so it never matched and re-fetched every few seconds. The poll now parses the response and compares the version field. Also drop the post-install chrome.runtime.reload() step: a fresh browser start always relaunches Chrome with --load-extension, which loads the new unpacked extension directly, and the hot-reload could leave the overlay’s content script uninjected until the next full restart.CLI teardown: reorganize the tooling around reusable library subsystems and add browser devtools.
- Separate
snapshot(observe: annotated tree + coverage) fromcapture(persist into a view’s set); extract theobserve,coverage,viewset, andauthoringpackages and merge the internalSessionintoCorpus. - Make the
browser startdaemon the single session owner: retire--launchand thebrowser launchsubcommand (live commands attach to a started session). - Add
consoleandnetworkdevtools commands, backed by a session-lifetime collector in the daemon that buffers console messages (with uncaught exceptions folded in) and network requests, with lazy response bodies. - Add screenshot clipping to
browser screenshot(--component/--selector/--expand-pct).