Context
While building a V-model gap-hunt pipeline that uses `rivet query --sexpr` to enumerate gaps (e.g., "every attack-scenario must have at least one outbound `exploits` link"), I hit two ergonomic gaps in the s-expression DSL.
1. No clean predicate for "artifact has no outbound link of type X"
The natural V-model question is: "find every attack-scenario with zero outbound `exploits` links." I tried the obvious shapes:
```
(and (= type "attack-scenario") (not (linked-from "exploits")))
(and (= type "attack-scenario") (not (linked-via "exploits"))) # unknown form
(and (= type "attack-scenario") (not (linked-as "exploits"))) # unknown form
(and (= type "attack-scenario") (not (out-link "exploits"))) # unknown form
(and (= type "attack-scenario") (not (has-link "exploits"))) # unknown form
```
`(not (linked-from "exploits"))` returns ALL attack-scenarios — including AS-1 which has `exploits → UCA-9` and `exploits → DF-1`. So `linked-from` does not mean "is the source of an outbound link of this type." Its semantics are something else (target-of-inbound-from-a-typed-source?), but the docs don't say.
`linked-to ` works clearly ("has outbound link to anything of this type/id") but doesn't let me filter by link-type. So `(linked-to "uca")` finds attack-scenarios that link to any UCA via any link-type, not specifically via `exploits`.
Suggestion: introduce `(linked-via )` (or pick a name) meaning "has at least one outbound link of this link-type." This is the inverse of the existing `(linked-by )` (which I think means "has at least one inbound link of this link-type"). With both, gap-hunt becomes one-line s-expr per category:
```
(and (= type "attack-scenario") (not (linked-via "exploits"))) ; AS missing outbound exploits
(and (= type "requirement") (not (linked-by "verifies"))) ; REQ missing inbound verifies
(and (= type "hazard") (not (linked-by "prevents"))) ; H missing inbound prevents
```
2. `linked-*` operator family needs documentation
The error message lists the operator family vaguely:
```
note: unknown head symbol; see docs/getting-started.md for the supported forms
(and/or/not/implies/excludes/=/!=/>/</has-tag/has-field/in/matches/contains/linked-*)
```
`linked-*` suggests a family but doesn't enumerate it. `rivet docs query` returns "Unknown topic". I had to discover `linked-to`, `linked-by`, `linked-from` empirically via trial-and-error, and even after discovery the semantic distinction between them isn't clear without inspecting source.
Suggestion: add a `rivet docs query` topic (and possibly a CLI flag like `rivet query --list-operators`) that enumerates every operator with a one-line example. Bonus: add the s-expr DSL reference to the README so it's discoverable.
Real-world impact
This came up while building `scripts/vmodel/` in sigil — a Mythos-style V-model gap-hunt pipeline (PR #86 / #87). The pipeline wants to enumerate gaps via `rivet query --sexpr`, but the missing primitive forced a fallback to `rivet validate --format json | python3 ...` parsing. That works but defeats the whole point of having a query DSL.
Concrete sample I tried (sigil repo)
```bash
rivet query --sexpr '(and (= type "attack-scenario") (not (linked-from "exploits")))' --format ids
Returned all 37 AS-N, including AS-1 which clearly has 'exploits' outbound links.
Expected: AS-34, AS-35, AS-36 (the three with 0 exploits, per rivet validate).
```
Context
While building a V-model gap-hunt pipeline that uses `rivet query --sexpr` to enumerate gaps (e.g., "every attack-scenario must have at least one outbound `exploits` link"), I hit two ergonomic gaps in the s-expression DSL.
1. No clean predicate for "artifact has no outbound link of type X"
The natural V-model question is: "find every attack-scenario with zero outbound `exploits` links." I tried the obvious shapes:
```
(and (= type "attack-scenario") (not (linked-from "exploits")))
(and (= type "attack-scenario") (not (linked-via "exploits"))) # unknown form
(and (= type "attack-scenario") (not (linked-as "exploits"))) # unknown form
(and (= type "attack-scenario") (not (out-link "exploits"))) # unknown form
(and (= type "attack-scenario") (not (has-link "exploits"))) # unknown form
```
`(not (linked-from "exploits"))` returns ALL attack-scenarios — including AS-1 which has `exploits → UCA-9` and `exploits → DF-1`. So `linked-from` does not mean "is the source of an outbound link of this type." Its semantics are something else (target-of-inbound-from-a-typed-source?), but the docs don't say.
`linked-to ` works clearly ("has outbound link to anything of this type/id") but doesn't let me filter by link-type. So `(linked-to "uca")` finds attack-scenarios that link to any UCA via any link-type, not specifically via `exploits`.
Suggestion: introduce `(linked-via )` (or pick a name) meaning "has at least one outbound link of this link-type." This is the inverse of the existing `(linked-by )` (which I think means "has at least one inbound link of this link-type"). With both, gap-hunt becomes one-line s-expr per category:
```
(and (= type "attack-scenario") (not (linked-via "exploits"))) ; AS missing outbound exploits
(and (= type "requirement") (not (linked-by "verifies"))) ; REQ missing inbound verifies
(and (= type "hazard") (not (linked-by "prevents"))) ; H missing inbound prevents
```
2. `linked-*` operator family needs documentation
The error message lists the operator family vaguely:
```
note: unknown head symbol; see docs/getting-started.md for the supported forms
(and/or/not/implies/excludes/=/!=/>/</has-tag/has-field/in/matches/contains/linked-*)
```
`linked-*` suggests a family but doesn't enumerate it. `rivet docs query` returns "Unknown topic". I had to discover `linked-to`, `linked-by`, `linked-from` empirically via trial-and-error, and even after discovery the semantic distinction between them isn't clear without inspecting source.
Suggestion: add a `rivet docs query` topic (and possibly a CLI flag like `rivet query --list-operators`) that enumerates every operator with a one-line example. Bonus: add the s-expr DSL reference to the README so it's discoverable.
Real-world impact
This came up while building `scripts/vmodel/` in sigil — a Mythos-style V-model gap-hunt pipeline (PR #86 / #87). The pipeline wants to enumerate gaps via `rivet query --sexpr`, but the missing primitive forced a fallback to `rivet validate --format json | python3 ...` parsing. That works but defeats the whole point of having a query DSL.
Concrete sample I tried (sigil repo)
```bash
rivet query --sexpr '(and (= type "attack-scenario") (not (linked-from "exploits")))' --format ids
Returned all 37 AS-N, including AS-1 which clearly has 'exploits' outbound links.
Expected: AS-34, AS-35, AS-36 (the three with 0 exploits, per rivet validate).
```