Scope & the Haystack Block View
Overview
Most search tools let you narrow by folder or by date. Neither matches how litigation actually works. A motion is not a file in a folder — it is the centre of a small constellation: the response that answers it, the reply that answers the response, the exhibits attached to each, the proposed order, the amended version that supersedes it, and the order that finally resolves it.
The Haystack Block View at /scope is where you see that constellation and act on it. It does two jobs:
- Filtering — draw a scope by selecting nodes on a canvas. Select a motion and you get the motion plus everything connected to it. That selection becomes the scope every subsequent search runs inside.
- Editor — fix the graph when it is wrong. Filings that arrived without a clear parent show up as a worklist; you connect them, and the cascade starts walking real edges.
The intended loop is: filter → notice the graph isn't ready → editor → connect → back to filter.
Why a graph, and why Haystack
Sound Suite models court records using the tag and relationship idiom from Project Haystack, with XETO as the schema layer that says what the tags mean.
That sounds like an odd borrowing — Haystack was built for building automation, for chillers and air handlers and sensor points. It fits here because the two domains have the same shape: entities, containment, relationships, state markers, and rules that fire off them.
The mapping Sound Suite uses:
| Haystack concept | Court equivalent | Why |
|---|---|---|
site |
Case | The outermost container. Everything belongs to exactly one. |
equip |
Motion | Contains things, and nests — an amended motion is a sub-equip of the original. |
point |
Filing event | Received / Filed / Responded, each carrying a fileRef to the document. |
| Tags | Markers and values | motion, signed, opposed, filingDate, jurisdiction. |
| Refs | Typed edges | respondingTo, replyingTo, resolves, amends. |
What this buys you that a folder tree cannot
Amendment chains nest naturally. A Motion to Compel, its First Amended version, and its Second Amended version form a chain — not three unrelated files that happen to share a word in the title. Amendment is modelled as a reusable Amendable mixin (amends / supersedes / revisionSeq), so it applies uniformly to motions, proposed orders, briefs, responses and exhibit lists. An amended proposed order is first-class, not a special case.
People are one pool, with roles layered on. A single Person record is tagged intrinsically — person, lawyer, judge. But the same attorney may be the movant on one motion and the respondent on another. Those contextual roles live on separate PersonRole records that bind a Person to a scope, so the canonical Person stays immutable. Search by tag returns people; search by role returns roles, joined back to the person.
Open-set tags mean the schema doesn't have to be finished. A jurisdiction with a filing type nobody anticipated gets a tag, not a migration. XETO then validates that the tag combination is coherent — at runtime, via @haxall/haxall's Namespace.fits(), with no offline compile step.
Queries read like the question you asked. Haystack filter syntax compiles to SQL against a tags JSON column, so motion and signed and not opposed is a real query, not a UI you have to build a form for.
The Filtering tab
The canvas renders your corpus as connected blocks: cases containing typed filings, with edges to the responses, replies, attachments and orders that hang off them.
Cascading selection
Selection cascades along real relationships:
- Select a case → every filing in it.
- Select a motion → that motion, plus its responses, replies, amendments and attachments.
- Deselect individually to carve exceptions out of a broad selection.
This is the part a folder picker cannot do. "This motion and everything that argues about it" is one click, and it stays correct as the case grows.
Scope overrides everything else
While a scope selection is active it overrides the case dropdown and any saved preset. The scope is written to your browser's local storage under search.scopeSet, with search.scopeSetActive as the on/off switch, and the search UI reads those directly.
Clearing the selection sets search.scopeSetActive to false but keeps the set on disk — so turning scope back on restores your last selection rather than making you rebuild it.
Entry points
| URL | Opens |
|---|---|
/scope |
Whole corpus, Filtering tab |
/scope?tab=editor |
Straight into the Editor |
/scope?case=<id> |
Narrowed to one case |
/scope?focus=<kind>:<id> |
Editor, with one entity preselected |
The Editor tab
Filtering is only as good as the graph beneath it. A filing whose parent was never identified is invisible to the cascade — select the motion it responds to, and it won't come along.
The Editor is the repair bench. It shows unconnected filings and entities with missing references as a worklist. Selecting one opens the structured tag panel, where you assign the relationships that matter:
| Reference | Meaning |
|---|---|
motionRef |
Which motion this filing belongs to |
respondingTo |
This filing answers that one |
replyingTo |
This filing replies to a response |
resolves |
An order, judgment or decree → the motion it rules on |
amends / supersedes |
Amendment chain links |
judgeRef |
The judge on this specific motion |
movantRef / respondentRef |
Party roles for this motion |
authoredBy |
Who wrote it |
A filing can be several things at once — a document may be both a motion and an attachment — so the editor tracks which underlying record owns each relationship slot before it writes an edge.
There is also an unfiled documents view: documents that no filing has claimed at all. These are usually loose PDFs that landed in a watched folder before anyone decided what they were.
Reading the counts
Each block shows docCount and indexedCount. When they diverge, some documents in that filing are still processing or failed to index — worth knowing before you trust a search that claims to cover it.
A worked example
A discovery dispute, with invented names throughout:
CAUSE NO. 00-0000-XX (case / site)
│
├── Motion to Compel Discovery (motion / equip)
│ ├── Filed 2026-01-15 (event / point)
│ ├── Response in Opposition respondingTo → Motion to Compel
│ │ └── Exhibit A — Correspondence attachment
│ ├── Reply in Support replyingTo → Response
│ └── Order Granting in Part resolves → Motion to Compel
│
└── First Amended Motion to Compel amends → Motion to Compel
└── Filed 2026-02-03 (event / point)
Selecting Motion to Compel Discovery in the Filtering tab scopes your search to all eight records — the motion, its lifecycle events, the opposition, that opposition's exhibit, the reply, the order, and the amended version. Ask "what did the other side say about proportionality?" and the answer is drawn from that set alone, not from every document in the case.
If the Reply in Support had arrived without a replyingTo reference, it would sit in the Editor worklist instead, and the cascade would have returned seven records while quietly missing the eighth. That is the failure the Editor exists to prevent.
Where the data comes from
The canvas is fed by three read endpoints:
| Endpoint | Returns |
|---|---|
GET /api/scope/graph |
Cases, filings, and the refs that connect them |
GET /api/scope/unconnected |
Filings missing a parent reference |
GET /api/scope/unfiled |
Documents no filing claims |
Everything runs against your local index. No part of the scope graph leaves your machine.
Related
- Search — using a scope once you've drawn one
- User Guide — the dashboard end to end
- MCP Tools Reference — querying the same graph from an AI assistant