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