Documents / Issues / issue-9b2d2ab09060
No expression language over the corpus
Both halves shipped: query --expr asks a question, and checks: in docs-schema.yaml states a rule the store runs.
Context¶
docir has rich structure and exactly one way to interrogate it: the filters query
ships with (--type --status --tag --owner --stale --code, plus --limit/--offset).
Every question outside that set is a feature request, and the set only ever grows in
one direction.
Questions the corpus can already answer but the CLI cannot ask:
- "Which decisions are past their cadence and govern code no one owns?"
- "Which documents have no inbound edges except
relates_to?" — i.e. nothing depends on them, which is different fromorphan. - "Which issues reference a decision that has since been superseded?"
- "Which types have more open than closed documents?"
Each is a projection or a join over data the index already holds. Answering them today
means either piping query --limit 10000 into jq — which throws away the graph, since
skeletons carry edges but nothing resolves them — or adding a flag.
Why the adjacent market treats this as the product¶
DocHub's pitch is architecture as data:
JSONata over the model, and its validators are the same expressions with "must return
empty" attached (ref-a6db21f52427, gap 14). It is the one thing in that comparison docir
has no answer to, and the one docir is closest to being able to give — the structure is
there, the vocabulary is not.
The tension this has to respect¶
adr-b2cfed9d5888 refused a rule engine, and that refusal must survive this. What it
refused was docir shipping opinions about your architecture plus the machinery those
imply: a rule DSL of docir's own, a sandbox for user-supplied code, per-language static
analysis. A user writing a predicate over their own documents' metadata is not that — it
is the same act as writing --owner platform-team --stale, with a grammar instead of a
flag.
The line to hold: docir ships no rules, only the ability to state one. If a shipped default check ever appears in this feature, it has crossed back.
Shape (not a decision — that is an ADR's job)¶
- An expression over a document's own fields and its resolved edges. JMESPath is the small, well-specified candidate: one dependency, a real grammar, no evaluation of arbitrary code. JSONata is more capable and much larger. A hand-rolled mini-language is the option to avoid.
query --expr '<expression>', applied as a post-SQL predicate before the limit — the seam--staleand--codealready share (_post_sql_predicate/_scanned_page), so--expr ... --limit 10means ten matching documents.- Named checks a store declares, in
docs-schema.yamlbeside the types they concern, evaluated bycheckas Tier 1 warnings. A store's own rule is exactly the kind of finding that must not gate CI on the schema's release cadence (the argumentschema-driftandmissing-requiredalready make). - The expression sees a document projection, not the ORM: id, type, status, tags, owner, verified, stale, code, and edges with their kinds in both directions. That is the contract to pin, because it is what a user's expression will be written against and what cannot then change silently.
What shipped¶
The filter half shipped as docir query --expr '<JMESPath>' — adr-7316abc6be93 records the
choices. Three of the four questions above are now askable:
docir query --expr "stale && owner == `null`"
docir query --type issue --expr "related[?status=='superseded']"
The fourth — "which types have more open than closed documents" — is out of reach by construction. It is a question about the corpus and this is a per-document predicate.
The three open questions, answered¶
- Resolved neighbours, not ids. Decided by the motivating case: "an issue pointing at a superseded decision" is a question about the target's status, so ids alone would have shipped a grammar without the case that justified it. The whole edge graph is read once per query and indexed both ways, so the cost does not grow with how many documents survive the SQL filters.
- Not answered, because named checks are not built. Severity only matters once a store's own rule can produce a finding, and that half is deliberately deferred.
- Yes, MCP.
docir_querytakesexpr, with the projection spelled out in its description. An agent that can state a question does not need a flag minted for each one, and an invalid expression is aValidationErrorlike an unknown tag.
What is left¶
Nothing. Both halves shipped — adr-d2ae4604a01e added checks: in docs-schema.yaml, and
question 2 is answered there: a declared check is always a warning, because --strict gates
on docir's own error kinds and must mean the same thing in every repository. --strict-all
covers a store that wants its rules fatal.
Building the filter first was deliberate, and waiting for a real rule was the rest of it. The rule that arrived — "something supersedes this and it is still live" — found two violations in docir's own corpus on its first run and went silent once they were retired. That is the feature's first exercise being a real question rather than a fixture.
Why the rest is still not urgent¶
The filter half is built, so the questions above have an answer that is not jq. What
remains — checks a store runs unasked — is the half that decides whether docir stays a
retrieval tool or becomes one an organisation governs with, and it carries the risk the
filter does not. Build it when someone actually hits it, with their rule as the first test.