@@ -39,6 +39,13 @@ fn rivet_bin() -> PathBuf {
3939
4040/// Minimal schema: one artifact type, one link type with inverse, plus
4141/// whatever else is needed for validation to accept a clean project.
42+ ///
43+ /// Both `satisfies` and `satisfied-by` are declared as authorable link
44+ /// types so that `rivet check bidirectional` fires when the inverse is
45+ /// missing (per #648, the oracle skips forward links whose declared
46+ /// inverse is not itself authorable). Test 1 (`_passes_when_every_forward_link_has_inverse`)
47+ /// and test 2 (`_fires_when_inverse_missing`) both rely on this schema
48+ /// staying bidirectionally-authorable.
4249const MINIMAL_SCHEMA : & str = r#"schema:
4350 name: oracle-test
4451 version: "0.1.0"
@@ -57,6 +64,12 @@ link-types:
5764 description: Source satisfies target
5865 source-types: [design-decision]
5966 target-types: [requirement]
67+
68+ - name: satisfied-by
69+ inverse: satisfies
70+ description: Target is satisfied by source
71+ source-types: [requirement]
72+ target-types: [design-decision]
6073"# ;
6174
6275const MINIMAL_RIVET_YAML : & str = r#"project:
@@ -198,6 +211,126 @@ fn bidirectional_fires_when_inverse_missing() {
198211 assert_eq ! ( viols[ 0 ] [ "expected_inverse" ] , "satisfied-by" ) ;
199212}
200213
214+ /// Schema that declares `satisfies` with an inverse name but does NOT
215+ /// declare that inverse (`satisfied-by`) as its own authorable link type.
216+ /// This is the shape called out in #648 (aspice embeds `satisfies`,
217+ /// `verifies`, `derives-from` this way): the inverse cannot be
218+ /// materialized without failing `rivet validate` — so the bidirectional
219+ /// oracle must treat the forward link as sufficient.
220+ const UNIDIRECTIONAL_SCHEMA : & str = r#"schema:
221+ name: oracle-test-uni
222+ version: "0.1.0"
223+ description: Schema whose `satisfies` inverse is not itself authorable.
224+
225+ artifact-types:
226+ - name: requirement
227+ description: A requirement
228+
229+ - name: design-decision
230+ description: A design decision
231+
232+ link-types:
233+ - name: satisfies
234+ inverse: satisfied-by
235+ description: Source satisfies target
236+ source-types: [design-decision]
237+ target-types: [requirement]
238+ "# ;
239+
240+ const UNIDIRECTIONAL_RIVET_YAML : & str = r#"project:
241+ name: oracle-test-uni
242+ version: "0.1.0"
243+ schemas:
244+ - oracle-test-uni
245+ sources:
246+ - path: artifacts
247+ format: generic-yaml
248+ "# ;
249+
250+ fn seed_unidirectional_project ( dir : & Path ) {
251+ std:: fs:: create_dir_all ( dir. join ( "schemas" ) ) . unwrap ( ) ;
252+ std:: fs:: create_dir_all ( dir. join ( "artifacts" ) ) . unwrap ( ) ;
253+ std:: fs:: write ( dir. join ( "rivet.yaml" ) , UNIDIRECTIONAL_RIVET_YAML ) . unwrap ( ) ;
254+ std:: fs:: write (
255+ dir. join ( "schemas" ) . join ( "oracle-test-uni.yaml" ) ,
256+ UNIDIRECTIONAL_SCHEMA ,
257+ )
258+ . unwrap ( ) ;
259+ }
260+
261+ /// #648 kill criterion: on a schema whose `satisfies.inverse` is not
262+ /// declared as an authorable link type, a project that only authors
263+ /// forward `satisfies` links must reach `bidirectional: OK` *and* pass
264+ /// `rivet validate` at the same time. Before this fix, the oracle
265+ /// demanded a `satisfied-by` inverse on every REQ, but validate rejected
266+ /// that inverse as `unknown-link-type` — one oracle's green was another
267+ /// oracle's red.
268+ #[ test]
269+ fn bidirectional_and_validate_both_pass_when_inverse_is_not_authorable ( ) {
270+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
271+ let dir = tmp. path ( ) ;
272+ seed_unidirectional_project ( dir) ;
273+
274+ // DD-001 authors the forward `satisfies` link. REQ-001 authors
275+ // nothing — the schema forbids `satisfied-by`, and the fix means the
276+ // bidirectional oracle no longer demands it.
277+ write_artifact (
278+ dir,
279+ "req.yaml" ,
280+ r#"artifacts:
281+ - id: REQ-001
282+ type: requirement
283+ title: a requirement
284+ status: draft
285+ "# ,
286+ ) ;
287+ write_artifact (
288+ dir,
289+ "dd.yaml" ,
290+ r#"artifacts:
291+ - id: DD-001
292+ type: design-decision
293+ title: a design decision
294+ status: draft
295+ links:
296+ - type: satisfies
297+ target: REQ-001
298+ "# ,
299+ ) ;
300+
301+ // Half 1: bidirectional oracle green.
302+ let bi = run_rivet ( dir, & [ "check" , "bidirectional" , "--format" , "json" ] ) ;
303+ let bi_stdout = String :: from_utf8_lossy ( & bi. stdout ) ;
304+ let bi_stderr = String :: from_utf8_lossy ( & bi. stderr ) ;
305+ assert ! (
306+ bi. status. success( ) ,
307+ "expected bidirectional OK; stdout={bi_stdout}; stderr={bi_stderr}"
308+ ) ;
309+ let bi_v: serde_json:: Value =
310+ serde_json:: from_str ( & bi_stdout) . expect ( "bidirectional stdout must be valid JSON" ) ;
311+ assert_eq ! ( bi_v[ "oracle" ] , "bidirectional" ) ;
312+ assert_eq ! (
313+ bi_v[ "violations" ] . as_array( ) . unwrap( ) . len( ) ,
314+ 0 ,
315+ "expected zero bidirectional violations, got: {bi_stdout}"
316+ ) ;
317+
318+ // Half 2: validate green — no unknown-link-type error for the inverse,
319+ // because we never authored it.
320+ let val = run_rivet ( dir, & [ "validate" , "--format" , "json" ] ) ;
321+ let val_stdout = String :: from_utf8_lossy ( & val. stdout ) ;
322+ let val_stderr = String :: from_utf8_lossy ( & val. stderr ) ;
323+ assert ! (
324+ val. status. success( ) ,
325+ "expected validate PASS; stdout={val_stdout}; stderr={val_stderr}"
326+ ) ;
327+ // No `unknown-link-type` diagnostic anywhere in the validate output.
328+ assert ! (
329+ !val_stdout. contains( "unknown-link-type" ) && !val_stderr. contains( "unknown-link-type" ) ,
330+ "validate should not emit unknown-link-type; stdout={val_stdout}; stderr={val_stderr}"
331+ ) ;
332+ }
333+
201334// ── review-signoff oracle ──────────────────────────────────────────────
202335
203336#[ test]
0 commit comments