docir — design documents Graph

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.

issue-9b2d2ab09060issueresolved#cli#retrieval#schema
View as Markdown◉ View in graph

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 from orphan.
  • "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 --stale and --code already share (_post_sql_predicate / _scanned_page), so --expr ... --limit 10 means ten matching documents.
  • Named checks a store declares, in docs-schema.yaml beside the types they concern, evaluated by check as 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 argument schema-drift and missing-required already 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:

bash
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

  1. 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.
  2. 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.
  3. Yes, MCP. docir_query takes expr, 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 a ValidationError like 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.

To amend: Re-verify: