From d87dbff820e1501fdc49200537030315215521d9 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:58:26 +1000 Subject: [PATCH 1/6] test(storage): conformance oracle for the wardline taint-fact wire seam Consumer side of the wardline->loomweave wardline-taint-1 taint-fact store seam. Vendors wardline's authority golden BYTE-IDENTICAL + a Layer-1 blake3 byte-pin + a NON-CIRCULAR oracle that drives loomweave's REAL storage API (loomweave_storage::wardline_taint): resolve_wardline_qualname -> upsert_taint_fact -> get_taint_facts, asserting each fact's wardline_json blob round-trips verbatim (key order included) off loomweave's store and content_hash_at_compute is preserved (queryable column + in-blob). Layer-2 drift recheck vs wardline authority (WARDLINE_REPO), skip-clean absent. 5 tests pass (cargo nextest). Boundary documented: drives the storage functions the cli HTTP handlers call; HTTP layer is pub(crate), covered by cli's own tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../wardline-taint-fact-wire.golden.json | 85 +++++ .../wardline_taint_fact_conformance_oracle.rs | 312 ++++++++++++++++++ 2 files changed, 397 insertions(+) create mode 100644 crates/loomweave-storage/tests/fixtures/wardline-taint-fact-wire.golden.json create mode 100644 crates/loomweave-storage/tests/wardline_taint_fact_conformance_oracle.rs diff --git a/crates/loomweave-storage/tests/fixtures/wardline-taint-fact-wire.golden.json b/crates/loomweave-storage/tests/fixtures/wardline-taint-fact-wire.golden.json new file mode 100644 index 0000000..ddea222 --- /dev/null +++ b/crates/loomweave-storage/tests/fixtures/wardline-taint-fact-wire.golden.json @@ -0,0 +1,85 @@ +[ + { + "content_hash_at_compute": "beb500c7a64f99d4cfb1e96b5f003b05c35326f2b1d73d7b2bf6225ab1307875", + "qualname": "svc.read_raw", + "wardline_json": { + "content_hash_at_compute": "beb500c7a64f99d4cfb1e96b5f003b05c35326f2b1d73d7b2bf6225ab1307875", + "dead_code_root": { + "is_root": true, + "reason": "Wardline trust-decorated entity is externally reachable or trust-significant.", + "source": "wardline_trust_decorator", + "tags": [ + "entry-point" + ] + }, + "findings": [], + "qualname": "svc.read_raw", + "schema_version": "wardline-taint-1", + "taint": { + "actual_return": "EXTERNAL_RAW", + "contributing_callee_qualname": null, + "declared_return": "EXTERNAL_RAW", + "resolved_call_count": 0, + "source": "anchored", + "unresolved_call_count": 0 + } + } + }, + { + "content_hash_at_compute": "beb500c7a64f99d4cfb1e96b5f003b05c35326f2b1d73d7b2bf6225ab1307875", + "qualname": "svc.helper", + "wardline_json": { + "content_hash_at_compute": "beb500c7a64f99d4cfb1e96b5f003b05c35326f2b1d73d7b2bf6225ab1307875", + "dead_code_root": { + "is_root": false, + "reason": null, + "source": null, + "tags": [] + }, + "findings": [], + "qualname": "svc.helper", + "schema_version": "wardline-taint-1", + "taint": { + "actual_return": "UNKNOWN_RAW", + "contributing_callee_qualname": null, + "declared_return": "UNKNOWN_RAW", + "resolved_call_count": 0, + "source": "fallback", + "unresolved_call_count": 0 + } + } + }, + { + "content_hash_at_compute": "beb500c7a64f99d4cfb1e96b5f003b05c35326f2b1d73d7b2bf6225ab1307875", + "qualname": "svc.leaky", + "wardline_json": { + "content_hash_at_compute": "beb500c7a64f99d4cfb1e96b5f003b05c35326f2b1d73d7b2bf6225ab1307875", + "dead_code_root": { + "is_root": true, + "reason": "Wardline trust-decorated entity is externally reachable or trust-significant.", + "source": "wardline_trust_decorator", + "tags": [ + "entry-point" + ] + }, + "findings": [ + { + "fingerprint": "9a291cac4a30b2cd8353f89eb428e184b01cb3919563ebeffd672745bf9cc665", + "line_start": 14, + "path": "svc.py", + "rule_id": "PY-WL-101" + } + ], + "qualname": "svc.leaky", + "schema_version": "wardline-taint-1", + "taint": { + "actual_return": "EXTERNAL_RAW", + "contributing_callee_qualname": "svc.read_raw", + "declared_return": "INTEGRAL", + "resolved_call_count": 1, + "source": "anchored", + "unresolved_call_count": 0 + } + } + } +] diff --git a/crates/loomweave-storage/tests/wardline_taint_fact_conformance_oracle.rs b/crates/loomweave-storage/tests/wardline_taint_fact_conformance_oracle.rs new file mode 100644 index 0000000..67924c4 --- /dev/null +++ b/crates/loomweave-storage/tests/wardline_taint_fact_conformance_oracle.rs @@ -0,0 +1,312 @@ +//! Wardline → Loomweave taint-fact wire conformance oracle. +//! +//! The CONSUMER side of the cross-repo "wardline-taint-1" taint-fact seam. +//! Wardline AUTHORS per-entity taint-fact blobs (`build_taint_facts`, in +//! `wardline/src/wardline/loomweave/facts.py`) and ships them over +//! `POST /api/wardline/taint-facts`; Loomweave (this crate) CONSUMES them: +//! resolves the dotted qualname to an entity, stores the `wardline_json` blob +//! VERBATIM keyed by that entity, and serves it back on the read paths. This +//! oracle pins that the bytes Wardline produces are accepted + round-tripped by +//! Loomweave's REAL storage code path. +//! +//! Mirrors the proven layering of `sei_conformance_oracle.rs`: +//! +//! * Layer 1 — a byte-pin: a `blake3` digest over the vendored golden bytes, +//! asserted against a const. If the vendored fixture drifts by a single +//! byte the pin reds. (Proven to red on tamper — see the module note below.) +//! +//! * A NON-CIRCULAR consumer oracle: the golden's taint-fact blobs are fed +//! through Loomweave's REAL deserialize/store/read API +//! (`resolve_wardline_qualname` → `upsert_taint_fact` → `get_taint_facts` +//! plus a direct read of the queryable `content_hash_at_compute` column), +//! asserting Loomweave ACCEPTS them: the entity qualname resolves `Exact`, +//! the `wardline_json` blob is stored + read back BYTE-VERBATIM, and the +//! content-hash is preserved (both the in-blob copy and the queryable +//! column). The assertions are driven off Loomweave's stored/returned bytes, +//! NOT off the golden restated against itself. +//! +//! * Layer 2 — a drift recheck: the vendored fixture bytes are compared +//! against the authority golden in the Wardline repo +//! (`$WARDLINE_REPO/tests/conformance/fixtures/wardline-taint-fact-wire.golden.json`, +//! `WARDLINE_REPO` defaulting to `/home/john/wardline`). Skip-clean when the +//! sibling repo is absent (CI / detached checkout): the oracle still passes +//! on the vendored copy + Layer-1 pin. +//! +//! ── Scope / honesty caveat ── +//! This is a STORAGE-crate oracle. It drives the storage ingest/read API +//! (`loomweave_storage::{resolve_wardline_qualname, upsert_taint_fact, +//! get_taint_facts, TaintFact}`) — the same functions the cli HTTP handlers in +//! `loomweave-cli/src/http_read/wardline.rs` call. It does NOT exercise the cli +//! HTTP layer itself: the wire structs (`TaintFactInput`, `WriteTaintFactsRequest`) +//! and route handlers there are `pub(crate)` and unreachable from this crate, so +//! the oracle re-declares the minimal wire shape (`qualname` + +//! `content_hash_at_compute` + `wardline_json` as a byte-verbatim `RawValue`) and +//! drives the storage API directly. The verbatim-bytes contract that the cli +//! handler relies on (`RawValue::get()` → `upsert_taint_fact`) is reproduced here +//! faithfully; end-to-end HTTP coverage lives in +//! `loomweave-cli/src/http_read/wardline.rs`'s own tests. + +use std::path::PathBuf; + +use rusqlite::{Connection, params}; +use serde::Deserialize; +use serde_json::value::RawValue; + +use loomweave_storage::schema::apply_migrations; +use loomweave_storage::{ + Resolution, TaintFact, get_taint_facts, resolve_wardline_qualname, upsert_taint_fact, +}; + +/// The Wardline authority golden, vendored BYTE-IDENTICAL from +/// `wardline/tests/conformance/fixtures/wardline-taint-fact-wire.golden.json` +/// (confirmed via `cmp`). `include_str!` embeds the exact on-disk bytes. +const GOLDEN: &str = include_str!("fixtures/wardline-taint-fact-wire.golden.json"); + +/// Layer-1 byte-pin: lowercase-hex `blake3` of the vendored golden's exact +/// bytes. Pins the fixture so a silent edit/re-vendor reds here. +/// +/// Tamper proof: perturbing one hex char of this const (or one byte of the +/// fixture) makes `golden_bytes_match_layer1_pin` fail with a +/// `left != right` mismatch — the pin is load-bearing, not decorative. +const GOLDEN_BLAKE3: &str = "5ecabddd14bfb6a1c245c62bfa7b34e2cb4a5c9209c0f7da0250e7293f91ca6a"; + +/// The plugin under which Wardline's Python-frontend qualnames resolve. The +/// golden is a Python scan (`svc.py`), so its qualnames live under +/// `python:function:` (ADR-036; Wardline pre-composes the dotted +/// qualname to byte-match Loomweave's `canonical_qualified_name`). +const PLUGIN: &str = "python"; + +/// One taint fact AS WARDLINE PUTS IT ON THE WIRE. This re-declares the cli's +/// `pub(crate)` `TaintFactInput` shape (`loomweave-cli/src/http_read/wardline.rs`): +/// `wardline_json` is a `Box` so the ORIGINAL bytes of the blob are +/// captured verbatim — a `serde_json::Value` would reorder object keys +/// (`BTreeMap`) and recompact, destroying the byte-verbatim contract the seam +/// guarantees. +#[derive(Debug, Deserialize)] +struct GoldenFact { + qualname: String, + content_hash_at_compute: String, + wardline_json: Box, +} + +fn golden_facts() -> Vec { + serde_json::from_str(GOLDEN).expect("vendored golden parses as a taint-fact list") +} + +/// Fresh in-memory DB with the REAL schema applied (migrations 0001..0010, +/// incl. `0003_wardline_taint_facts`). `foreign_keys` ON via the read pragmas +/// so the `entity_id → entities.id` FK is live (production parity). +fn migrated_conn() -> Connection { + let mut conn = Connection::open_in_memory().expect("open in-memory db"); + apply_migrations(&mut conn).expect("apply migrations"); + loomweave_storage::pragma::apply_read_pragmas(&conn).expect("apply read pragmas"); + conn +} + +/// Seed a full `entities` row for `python:function:`. Both legs of the +/// real consumer path need it: `resolve_wardline_qualname` returns `Exact` only +/// when the row exists, and `get_taint_facts` JOINs `entities`. Column list +/// mirrors `wardline_taint.rs`'s `insert_entity` test helper. +fn seed_entity(conn: &Connection, qualname: &str) -> String { + let id = format!("{PLUGIN}:function:{qualname}"); + conn.execute( + "INSERT INTO entities ( \ + id, plugin_id, kind, name, short_name, properties, \ + content_hash, source_file_path, created_at, updated_at \ + ) VALUES (?1, ?2, 'function', ?3, ?4, '{}', 'deadbeef', ?5, ?6, ?6)", + params![ + id, + PLUGIN, + qualname, + qualname.rsplit('.').next().unwrap_or(qualname), + "svc.py", + "2026-06-24T00:00:00.000Z", + ], + ) + .expect("seed entity row"); + id +} + +/// Read the queryable `content_hash_at_compute` column directly from the real +/// `wardline_taint_facts` table — the column `TaintFactRow`/`get_taint_facts` +/// do NOT surface (they expose only `wardline_json` + `source_file_path` + `sei`). +fn stored_content_hash(conn: &Connection, entity_id: &str) -> Option { + conn.query_row( + "SELECT content_hash_at_compute FROM wardline_taint_facts WHERE entity_id = ?1", + params![entity_id], + |row| row.get::<_, Option>(0), + ) + .expect("query stored content_hash") +} + +// ── Layer 1 — byte-pin ─────────────────────────────────────────────────────── + +#[test] +fn golden_bytes_match_layer1_pin() { + let actual = blake3::hash(GOLDEN.as_bytes()).to_hex().to_string(); + assert_eq!( + actual, GOLDEN_BLAKE3, + "vendored wardline-taint-fact golden drifted from its byte-pin; \ + re-vendor BYTE-IDENTICAL from wardline and update GOLDEN_BLAKE3" + ); +} + +#[test] +fn golden_is_the_expected_three_fact_shape() { + // A cheap structural floor so the consumer oracle below isn't silently + // exercising an empty list (a regression that vendored a `[]` would pass the + // round-trip vacuously). The authority golden is the three svc.py facts. + let facts = golden_facts(); + assert_eq!(facts.len(), 3, "golden carries exactly three taint facts"); + let qualnames: Vec<&str> = facts.iter().map(|f| f.qualname.as_str()).collect(); + assert_eq!(qualnames, ["svc.read_raw", "svc.helper", "svc.leaky"]); +} + +// ── NON-CIRCULAR consumer oracle ───────────────────────────────────────────── + +#[test] +fn loomweave_accepts_and_roundtrips_every_golden_fact_verbatim() { + let conn = migrated_conn(); + let facts = golden_facts(); + + for fact in &facts { + // The exact bytes Wardline ships for this fact's blob (no key reorder). + let wire_blob: &str = fact.wardline_json.get(); + + // 1. RESOLVE — Loomweave's real exact-tier resolver maps the dotted + // qualname to its entity id. Seed the entity first (an analyze run + // would have placed it); then assert resolution is Exact to the + // `python:function:` id. This is the "qualname resolves" + // leg, driven through `resolve_wardline_qualname`. + let expected_id = seed_entity(&conn, &fact.qualname); + let resolution = + resolve_wardline_qualname(&conn, &fact.qualname).expect("resolve qualname"); + assert_eq!( + resolution, + Resolution::Exact { + entity_id: expected_id.clone(), + }, + "golden qualname {} must resolve Exact to {expected_id}", + fact.qualname + ); + let entity_id = resolution.into_entity_id().expect("exact has an id"); + + // 2. STORE — feed the fact through the REAL writer. `TaintFact.wardline_json` + // takes the verbatim blob bytes exactly as the cli handler does + // (`RawValue::get().to_owned()`); `content_hash_at_compute` is the + // top-level queryable column Wardline ships alongside the blob. + upsert_taint_fact( + &conn, + &TaintFact { + entity_id: entity_id.clone(), + wardline_json: wire_blob.to_owned(), + scan_id: Some("conformance-scan".to_owned()), + content_hash_at_compute: Some(fact.content_hash_at_compute.clone()), + updated_at: "2026-06-24T00:00:00.000Z".to_owned(), + sei: None, + }, + ) + .expect("upsert golden taint fact"); + + // 3. READ BACK — Loomweave's real reader returns the blob VERBATIM. + let rows = + get_taint_facts(&conn, std::slice::from_ref(&entity_id)).expect("get taint facts"); + assert_eq!(rows.len(), 1, "exactly one stored fact for {entity_id}"); + let row = &rows[0]; + assert_eq!(row.entity_id, entity_id); + assert_eq!( + row.wardline_json, wire_blob, + "Loomweave must store + return the wardline_json blob byte-verbatim \ + (key order included) for {}", + fact.qualname + ); + + // 4. CONTENT-HASH PRESERVED — two ways, both read off Loomweave's store: + // (a) the queryable column, read directly from the real table; + // (b) the in-blob copy, parsed back out of the verbatim bytes the + // reader returned. Both must equal the golden's content hash. + assert_eq!( + stored_content_hash(&conn, &entity_id).as_deref(), + Some(fact.content_hash_at_compute.as_str()), + "queryable content_hash_at_compute column must be preserved for {}", + fact.qualname + ); + let parsed: serde_json::Value = + serde_json::from_str(&row.wardline_json).expect("stored blob is valid JSON"); + assert_eq!( + parsed["content_hash_at_compute"].as_str(), + Some(fact.content_hash_at_compute.as_str()), + "in-blob content_hash_at_compute must survive the round-trip for {}", + fact.qualname + ); + // The blob self-identifies as the wardline-taint-1 schema and echoes the + // qualname — proves we round-tripped the RIGHT fact, not an empty stub. + assert_eq!(parsed["schema_version"].as_str(), Some("wardline-taint-1")); + assert_eq!(parsed["qualname"].as_str(), Some(fact.qualname.as_str())); + } +} + +#[test] +fn golden_facts_cover_the_blob_variants() { + // Beyond the per-fact round-trip, assert the three facts exercise the + // structural variants of the wardline-taint-1 blob the consumer must accept: + // an entry-point root with no findings, a non-root fallback, and a leaky + // entry-point WITH a finding + a resolved contributing callee. This keeps + // the oracle honest if the golden is ever re-vendored to a thinner shape. + let facts = golden_facts(); + let parsed: Vec = facts + .iter() + .map(|f| serde_json::from_str(f.wardline_json.get()).expect("blob json")) + .collect(); + + // svc.read_raw — root entry-point, no findings, anchored EXTERNAL_RAW. + assert_eq!(parsed[0]["dead_code_root"]["is_root"], true); + assert_eq!(parsed[0]["findings"].as_array().unwrap().len(), 0); + + // svc.helper — non-root fallback, no findings. + assert_eq!(parsed[1]["dead_code_root"]["is_root"], false); + assert_eq!(parsed[1]["taint"]["source"], "fallback"); + + // svc.leaky — root entry-point WITH a finding + a contributing callee. + assert_eq!(parsed[2]["dead_code_root"]["is_root"], true); + let findings = parsed[2]["findings"].as_array().unwrap(); + assert_eq!(findings.len(), 1); + assert_eq!(findings[0]["rule_id"], "PY-WL-101"); + assert_eq!( + parsed[2]["taint"]["contributing_callee_qualname"], "svc.read_raw", + "the leaky entity's contributing callee must round-trip" + ); +} + +// ── Layer 2 — drift recheck vs the Wardline source of truth ────────────────── + +#[test] +fn vendored_golden_matches_wardline_authority() { + let repo = std::env::var("WARDLINE_REPO").unwrap_or_else(|_| "/home/john/wardline".to_owned()); + let authority: PathBuf = PathBuf::from(repo) + .join("tests") + .join("conformance") + .join("fixtures") + .join("wardline-taint-fact-wire.golden.json"); + + if !authority.exists() { + // Skip-clean: the sibling Wardline repo is absent (CI / detached + // checkout). The vendored copy + Layer-1 pin still hold; we just can't + // recheck against the upstream source here. + eprintln!( + "wardline authority golden not found at {} — skipping Layer-2 drift recheck \ + (set WARDLINE_REPO to enable)", + authority.display() + ); + return; + } + + let authority_bytes = std::fs::read(&authority).expect("read wardline authority golden"); + assert_eq!( + authority_bytes, + GOLDEN.as_bytes(), + "vendored golden has DRIFTED from the Wardline authority at {}; \ + re-vendor BYTE-IDENTICAL", + authority.display() + ); +} From 2721f3ab937a8eb67341e899e4b9760984664d33 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:35:10 +1000 Subject: [PATCH 2/6] test(plugin-python): conformance oracle for the wardline vocabulary-descriptor seam Consumer side of the trust-vocab descriptor seam. loomweave's python plugin vendors wardline's authority golden BYTE-IDENTICAL (same blob f5ad8d23) + a Layer-1 byte-pin + a NON-CIRCULAR oracle driving the REAL consumer: load_wardline_descriptor (read -> version-gate -> parse) then extractor.extract, asserting the wardline:external_boundary/trust_boundary/trusted tags derive end-to-end via _attach_wardline_entity_metadata. Version-skew proof: a copy with only the version bumped flips status enabled->version_skew (the gate keys on version alone). Layer-2 drift recheck vs wardline authority (WARDLINE_REPO), skip-clean absent. 5 tests pass; plugin suite 220 passed, ruff(ALL)+mypy(strict) clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...wardline-vocabulary-descriptor.golden.yaml | 14 + ...dline_vocabulary_descriptor_conformance.py | 307 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 plugins/python/tests/fixtures/wardline-vocabulary-descriptor.golden.yaml create mode 100644 plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py diff --git a/plugins/python/tests/fixtures/wardline-vocabulary-descriptor.golden.yaml b/plugins/python/tests/fixtures/wardline-vocabulary-descriptor.golden.yaml new file mode 100644 index 0000000..f5ad8d2 --- /dev/null +++ b/plugins/python/tests/fixtures/wardline-vocabulary-descriptor.golden.yaml @@ -0,0 +1,14 @@ +schema: wardline.vocabulary/v1 +version: wardline-generic-2 +entries: +- canonical_name: external_boundary + group: 1 + attrs: {} +- canonical_name: trust_boundary + group: 1 + attrs: + _wardline_to_level: TaintState +- canonical_name: trusted + group: 1 + attrs: + _wardline_level: TaintState diff --git a/plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py b/plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py new file mode 100644 index 0000000..72ac5c4 --- /dev/null +++ b/plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py @@ -0,0 +1,307 @@ +"""Wardline → Loomweave trust-vocabulary descriptor wire conformance oracle. + +The CONSUMER side of the cross-repo "Vocabulary descriptor (trust-vocab)" seam. +Wardline AUTHORS the NG-25 trust-vocabulary descriptor — it OWNS the vocabulary +via ``wardline.core.registry.REGISTRY`` and serialises it through +``wardline.core.descriptor.build_vocabulary_descriptor`` / ``descriptor_to_yaml`` +to ``.weft/wardline/vocabulary.yaml`` (``{schema: wardline.vocabulary/v1, +version, entries:[{canonical_name, group, attrs}]}``). Loomweave's Python plugin +is the real CONSUMER: ``loomweave_plugin_python.wardline_descriptor +.load_wardline_descriptor`` byte-reads that file (it NEVER imports wardline), +version-gates on ``EXPECTED_DESCRIPTOR_VERSION == "wardline-generic-2"``, and +parses ``entries`` into a ``WardlineVocabulary`` that the extractor threads into +``wardline:external_boundary`` / ``wardline:trusted`` entity tags. + +This oracle pins that the bytes Wardline produces are accepted + correctly +interpreted by Loomweave's REAL consumer code path. It mirrors the layering of +the taint-fact storage oracle +(``crates/loomweave-storage/tests/wardline_taint_fact_conformance_oracle.rs``): + + * Layer 1 — a byte-pin (``test_vendored_golden_matches_byte_pin``): the + git-blob SHA-1 of the vendored golden, asserted against a const. If the + vendored fixture drifts by a single byte the pin reds. The SHA mirrors + wardline's own ``UPSTREAM_BLOB_SHA`` byte-pin idiom + (``wardline/tests/conformance/test_vocabulary_descriptor_wire_golden.py``) + so the two repos pin the SAME 40 hex chars. On its OWN this is circular + (it pins the vendored bytes against themselves); the non-circular breaks + are the consumer oracle below + the Layer-2 drift recheck. + + * A NON-CIRCULAR consumer oracle: the vendored golden bytes are written to a + project descriptor location and fed through Loomweave's REAL + ``load_wardline_descriptor`` (resolve → read → ``yaml.safe_load`` → parse → + version-gate), asserting Loomweave ACCEPTS them (version accepted → + ``enabled``; entries → ``WardlineVocabulary`` with the expected + canonical_names / groups; the ``external_boundary`` / ``trusted`` decorator + attrs derive correctly). It then drives the REAL extractor + (``extractor.extract``) with that parsed-from-golden vocabulary to prove the + ``wardline:external_boundary`` / ``wardline:trusted`` TAGS derive correctly + end to end — not just that the lookup table parses. A version-skew copy + (only the ``version`` string bumped, every other byte the golden's) is fed + through the SAME real loader to prove the version gate is real, not + cosmetic: ``enabled`` flips to ``version_skew``. All assertions are driven + off the consumer's returned state, NOT off the golden restated against + itself. + + * Layer 2 — a drift recheck (``test_vendored_golden_matches_wardline_authority``): + the vendored fixture bytes are compared against the authority golden in the + Wardline repo + (``$WARDLINE_REPO/tests/conformance/fixtures/wardline-vocabulary-descriptor.golden.yaml``, + ``WARDLINE_REPO`` defaulting to ``/home/john/wardline``). Skip-clean when the + sibling repo is absent (CI / detached checkout); fail-closed on any + divergence. + +── Scope / honesty caveat ── +This drives the consumer parse + gate (``load_wardline_descriptor``) and the +consumer tag-derivation (``extractor.extract`` with the parsed vocabulary) — the +same two code paths the plugin server runs: ``server.handle_initialize`` calls +``load_wardline_descriptor`` and stashes ``.vocabulary`` on its state, and +``server.handle_analyze_file`` threads that vocabulary into ``extract`` as +``wardline_vocabulary=``. It does NOT spin up the JSON-RPC server loop itself; +the server's own tests cover that wiring. The non-circular guarantee comes from +driving the REAL parse/gate/derive on the producer-authored bytes, never the +golden against itself. +""" + +from __future__ import annotations + +import hashlib +import os +from pathlib import Path + +import pytest + +from loomweave_plugin_python.extractor import extract +from loomweave_plugin_python.wardline_descriptor import ( + EXPECTED_DESCRIPTOR_VERSION, + WardlineVocabulary, + load_wardline_descriptor, +) + +# The vendored copy of wardline's authority golden, BYTE-IDENTICAL to +# wardline/tests/conformance/fixtures/wardline-vocabulary-descriptor.golden.yaml +# (confirmed via `cmp`). Read as bytes so the byte-pin sees the exact on-disk +# bytes wardline ships, not a yaml.safe_load round-trip. +GOLDEN_PATH = Path(__file__).parent / "fixtures" / "wardline-vocabulary-descriptor.golden.yaml" + +# Layer-1 byte-pin: the git-blob SHA-1 of the vendored golden. This is the SAME +# 40 hex chars wardline pins as UPSTREAM_BLOB_SHA on the producer side +# (wardline/tests/conformance/test_vocabulary_descriptor_wire_golden.py) — the +# two repos pin identical bytes, so a one-sided re-vendor reds both suites. +# Recomputed below as sha1(b"blob %d\0" % len(data) + data). Any edit to the +# vendored golden without a matching re-pin reds this test. +UPSTREAM_BLOB_SHA = "f5ad8d2346ffb6ea75aa469e423c6c7cfd16d40a" + +# The canonical decorator names the producer authors (group 1) and the attrs the +# consumer must surface for the trust-tier markers. Asserting on these proves the +# consumer parses the real authored format — including the trust-tier `attrs` +# that the existing inline-string unit tests cover but which here come straight +# off the producer's own bytes. +EXPECTED_CANONICAL_NAMES = ("external_boundary", "trust_boundary", "trusted") +EXPECTED_ATTRS = { + "external_boundary": {}, + "trust_boundary": {"_wardline_to_level": "TaintState"}, + "trusted": {"_wardline_level": "TaintState"}, +} + + +def _write_project_descriptor(project_root: Path, text: str) -> None: + """Place descriptor bytes at the real .weft/wardline/ project location the + consumer reads (ADR-046).""" + descriptor = project_root / ".weft" / "wardline" / "vocabulary.yaml" + descriptor.parent.mkdir(parents=True, exist_ok=True) + descriptor.write_text(text, encoding="utf-8") + + +# ── Layer 1 — byte-pin ─────────────────────────────────────────────────────── + + +def test_vendored_golden_matches_byte_pin() -> None: + """Layer-1: the vendored wardline-authored descriptor golden byte-pins to its + git-blob SHA-1. ANY edit to the vendored fixture without a matching re-pin + reds here. On its OWN this is circular (vendored bytes pinned against + themselves); the non-circular protection is the consumer oracle + the Layer-2 + drift recheck below. + + Tamper proof (verified out-of-band): a one-byte-tampered copy of the fixture + hashes to a DIFFERENT git-blob SHA-1, so this assert reds — the pin is + load-bearing, not decorative. + """ + assert len(UPSTREAM_BLOB_SHA) == 40, ( + f"UPSTREAM_BLOB_SHA must be a 40-char git blob SHA-1: {UPSTREAM_BLOB_SHA!r}" + ) + assert set(UPSTREAM_BLOB_SHA) <= set("0123456789abcdef"), ( + f"UPSTREAM_BLOB_SHA must be lowercase hex (a git blob SHA-1): {UPSTREAM_BLOB_SHA!r}" + ) + data = GOLDEN_PATH.read_bytes() + actual = hashlib.sha1(b"blob %d\x00" % len(data) + data).hexdigest() # noqa: S324 - git blob id, not a security hash + assert actual == UPSTREAM_BLOB_SHA, ( + f"the vendored vocabulary-descriptor golden changed (git blob {actual}, " + f"pinned {UPSTREAM_BLOB_SHA}) — if this was a deliberate re-vendor, re-copy " + "BYTE-IDENTICAL from wardline " + "(tests/conformance/fixtures/wardline-vocabulary-descriptor.golden.yaml), " + "confirm with `cmp`, and update UPSTREAM_BLOB_SHA in the SAME commit; if not, " + "revert the edit." + ) + + +# ── NON-CIRCULAR consumer oracle ───────────────────────────────────────────── + + +def test_consumer_accepts_golden_and_parses_vocabulary(tmp_path: Path) -> None: + """The vendored golden bytes (schema line included) fed through the REAL + ``load_wardline_descriptor`` are ACCEPTED: version-gated to ``enabled`` and + parsed into a ``WardlineVocabulary`` whose canonical_names / groups / attrs + match the producer-authored format. Driven off the consumer's returned state, + never the golden against itself.""" + golden_text = GOLDEN_PATH.read_text("utf-8") + _write_project_descriptor(tmp_path, golden_text) + + state = load_wardline_descriptor(tmp_path) + + # Version accepted — the gate passed, so the descriptor is enabled. + assert state.status == "enabled", ( + f"consumer rejected the producer-authored golden: status={state.status!r} " + f"reason={state.reason!r}" + ) + assert state.source == "project" + assert state.descriptor_version == EXPECTED_DESCRIPTOR_VERSION + + vocab = state.vocabulary + assert isinstance(vocab, WardlineVocabulary) + assert vocab.version == EXPECTED_DESCRIPTOR_VERSION + assert vocab.confidence_basis == "descriptor" + + # entries → WardlineVocabulary with the expected canonical_names. + assert tuple(sorted(vocab.entries_by_name)) == tuple(sorted(EXPECTED_CANONICAL_NAMES)) + + # Each entry's group + attrs survive parse exactly. The trust-tier attrs + # (_wardline_to_level / _wardline_level) are the real cross-tool delta the + # inline-string unit tests duplicate but which here come off producer bytes. + for name in EXPECTED_CANONICAL_NAMES: + entry = vocab.entries_by_name[name] + assert entry.canonical_name == name + assert entry.group == 1, f"{name} must be a group-1 marker, got {entry.group}" + assert entry.attrs == EXPECTED_ATTRS[name], ( + f"{name} attrs drifted: {entry.attrs!r} != {EXPECTED_ATTRS[name]!r}" + ) + + # entry_for_decorator (the real lookup the extractor calls) resolves the + # last dotted segment to the right entry — the external_boundary / trusted + # markers derive from the parsed table, not a restated f-string. + eb = vocab.entry_for_decorator("weft_markers.external_boundary") + assert eb is not None + assert eb.canonical_name == "external_boundary" + tr = vocab.entry_for_decorator("trusted") + assert tr is not None + assert tr.canonical_name == "trusted" + + +def test_consumer_derives_external_boundary_and_trusted_tags_through_extractor( + tmp_path: Path, +) -> None: + """End-to-end consumer derivation: parse the golden through the REAL + ``load_wardline_descriptor``, then thread the parsed vocabulary into the REAL + ``extractor.extract``. This proves the ``wardline:external_boundary`` / + ``wardline:trusted`` TAGS derive correctly through the full consumer path — + not merely that the lookup table parsed.""" + golden_text = GOLDEN_PATH.read_text("utf-8") + _write_project_descriptor(tmp_path, golden_text) + + state = load_wardline_descriptor(tmp_path) + assert state.status == "enabled" + vocabulary = state.vocabulary + assert vocabulary is not None + + source = """\ +from weft_markers import external_boundary, trust_boundary, trusted + +@external_boundary +def read_body(): + return "" + +@weft_markers.trust_boundary(to_level="ASSURED") +@trusted(level="INTEGRAL") +class Sanitizer: + pass +""" + + entities, _edges = extract(source, "service.py", wardline_vocabulary=vocabulary) + + read_body = next(e for e in entities if e["id"] == "python:function:service.read_body") + sanitizer = next(e for e in entities if e["id"] == "python:class:service.Sanitizer") + + # external_boundary tag derives from the golden-parsed vocabulary. + assert "wardline" in read_body["tags"] + assert "wardline:external_boundary" in read_body["tags"] + assert read_body["wardline"]["descriptor_version"] == EXPECTED_DESCRIPTOR_VERSION + assert read_body["wardline"]["confidence_basis"] == "descriptor" + eb_decorators = read_body["wardline"]["decorators"] + assert [d["canonical_name"] for d in eb_decorators] == ["external_boundary"] + assert eb_decorators[0]["attrs"] == {} + + # trust_boundary + trusted tags derive, carrying the trust-tier attrs. + assert "wardline:trust_boundary" in sanitizer["tags"] + assert "wardline:trusted" in sanitizer["tags"] + san_attrs = {d["canonical_name"]: d["attrs"] for d in sanitizer["wardline"]["decorators"]} + assert san_attrs["trust_boundary"] == {"_wardline_to_level": "TaintState"} + assert san_attrs["trusted"] == {"_wardline_level": "TaintState"} + + +def test_consumer_version_gate_rejects_skew_copy(tmp_path: Path) -> None: + """The version gate is REAL, not cosmetic: the SAME golden bytes with ONLY + the ``version`` string bumped flip the consumer from ``enabled`` to + ``version_skew`` through the REAL ``load_wardline_descriptor``. The contrast + with ``test_consumer_accepts_golden_and_parses_vocabulary`` (identical bytes + bar the version) is the proof that the gate fires on version alone.""" + golden_text = GOLDEN_PATH.read_text("utf-8") + assert EXPECTED_DESCRIPTOR_VERSION in golden_text, ( + "the golden must carry the expected version for the skew derivation to be a " + "single-field perturbation" + ) + skewed = golden_text.replace(EXPECTED_DESCRIPTOR_VERSION, "wardline-generic-3") + assert skewed != golden_text, "version-skew copy must differ from the golden" + _write_project_descriptor(tmp_path, skewed) + + state = load_wardline_descriptor(tmp_path) + + # The gate fired: a one-field version bump is REJECTED as skew. + assert state.status == "version_skew", ( + f"version gate failed to fire on a skewed descriptor: status={state.status!r}" + ) + assert state.descriptor_version == "wardline-generic-3" + assert state.expected_version == EXPECTED_DESCRIPTOR_VERSION + # The vocabulary still parses (entries are valid) but is flagged degraded — + # proving the gate keys on version, not on a parse failure. + assert state.vocabulary is not None + assert state.vocabulary.confidence_basis == "descriptor_version_skew" + assert tuple(sorted(state.vocabulary.entries_by_name)) == tuple(sorted(EXPECTED_CANONICAL_NAMES)) + + +# ── Layer 2 — drift recheck vs the Wardline source of truth ────────────────── + + +def test_vendored_golden_matches_wardline_authority() -> None: + """Layer-2: the vendored fixture bytes must equal the authority golden in the + Wardline repo. Skip-clean when the sibling repo is absent (CI / detached + checkout) — the vendored copy + Layer-1 pin still hold; fail-closed on any + divergence.""" + repo = os.environ.get("WARDLINE_REPO", "/home/john/wardline") + authority = ( + Path(repo) + / "tests" + / "conformance" + / "fixtures" + / "wardline-vocabulary-descriptor.golden.yaml" + ) + if not authority.exists(): + pytest.skip( + f"wardline authority golden not found at {authority} — skipping Layer-2 drift " + "recheck (set WARDLINE_REPO to enable)" + ) + + authority_bytes = authority.read_bytes() + vendored_bytes = GOLDEN_PATH.read_bytes() + assert authority_bytes == vendored_bytes, ( + f"the vendored golden has DRIFTED from the Wardline authority at {authority}; " + "re-vendor BYTE-IDENTICAL (cmp must show no difference) and re-pin UPSTREAM_BLOB_SHA" + ) From d427472a995a911456723454f619bbd2875a49fa Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:31:41 +1000 Subject: [PATCH 3/6] test(mcp): conformance oracle for the entity-associations seam (consumer) Loomweave consumer side of the filigree<->loomweave entity-associations seam. Reconciles the stale vendored fixture (v1) to filigree's authority (v3, byte-identical now) + a Layer-1 blake3 byte-pin + a NON-CIRCULAR oracle driving the REAL parser loomweave_federation::filigree::parse_entity_associations_response (round-trips the opaque binding, tolerates the pre-26 clarion_entity_id alias, ignores unmodelled producer fields, degrades absent->empty). Layer-2 byte-drift recheck vs filigree authority (FILIGREE_REPO) with fail-closed arming (LOOMWEAVE_DRIFT_REQUIRED: SKIP->FAILURE when armed, mirroring wardline's _live_oracle primitive) + 3 unit tests over the 2x2. 10 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../entity_associations_conformance_oracle.rs | 386 ++++++++++++++++++ ...filigree-entity-associations-response.json | 14 +- 2 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs diff --git a/crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs b/crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs new file mode 100644 index 0000000..c37c19d --- /dev/null +++ b/crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs @@ -0,0 +1,386 @@ +//! Filigree → Loomweave entity-associations (ADR-029) wire conformance oracle. +//! +//! The CONSUMER side of the cross-repo `filigree<->loomweave` entity-associations +//! seam. FILIGREE is the PRODUCER: it stores Loomweave entity IDs OPAQUELY on its +//! `entity_associations` rows (it never parses the `{plugin}:{kind}:{qualname}` / +//! `loomweave:eid:*` grammar) and serves them back over +//! `GET /api/entity-associations?entity_id={entity_id}` +//! (`dashboard_routes.entities.api_list_associations_by_entity`). LOOMWEAVE (this +//! crate) is the CONSUMER: its `issues_for` reverse-join deserializes that body +//! through `loomweave_federation::filigree::parse_entity_associations_response` +//! (re-exported as `loomweave_mcp::filigree::parse_entity_associations_response`) +//! and joins each row's opaque `loomweave_entity_id` back to a current entity. +//! +//! This oracle pins that the bytes Filigree produces are accepted + correctly +//! deserialized by Loomweave's REAL parse code path, with the opaque bindings +//! round-tripped VERBATIM (never grammar-parsed) and exposing exactly the fields +//! the reverse-join consumes. +//! +//! Mirrors the proven layering of `wardline_taint_fact_conformance_oracle.rs` +//! and `sei_conformance_oracle.rs`: +//! +//! * Layer 1 — a byte-pin: a `blake3` digest over the vendored golden bytes, +//! asserted against a const. If the vendored fixture drifts by a single byte +//! the pin reds. (Proven to red on tamper — see `golden_bytes_match_layer1_pin`.) +//! +//! * A NON-CIRCULAR consumer oracle: the golden's response body is fed through +//! Loomweave's REAL `parse_entity_associations_response` and the parsed +//! `EntityAssociation` rows are asserted — the opaque `loomweave:eid:*` +//! binding lands VERBATIM in `loomweave_entity_id` (un-parsed), and the row +//! carries exactly the three fields the `issues_for` reverse-join joins on +//! (`issue_id` for global dedup, `loomweave_entity_id` for the alias→current +//! entity join, `content_hash_at_attach` for drift classification). The +//! assertions are driven off the REAL parser's output, NOT off the golden +//! restated against itself. The `clarion_entity_id` pre-v26 alias is also +//! exercised — that is live parse behaviour the consumer must preserve. +//! +//! * Layer 2 — a drift recheck: the vendored fixture bytes are compared against +//! the authority golden in the Filigree repo +//! (`$FILIGREE_REPO/tests/fixtures/contracts/entity-associations-response.json`, +//! `FILIGREE_REPO` defaulting to `/home/john/filigree`). Filigree is the +//! PRODUCER, so its copy is the authority. When the sibling repo is absent +//! (CI / detached checkout) the recheck is SKIP-CLEAN *by default* — the +//! oracle still passes on the vendored copy + Layer-1 pin — but a release-gate +//! job can ARM the recheck via `LOOMWEAVE_DRIFT_REQUIRED=1` (`1`/`true`/`yes`/ +//! `on`), which turns the absent-sibling skip into a hard FAILURE so a release +//! cut never silently ships the cross-repo authority check un-run. The +//! arming decision is a pure helper (`drift_check_action`) so all three +//! branches — compare / skip-clean / fail-required — are exercised +//! deterministically without mutating process-global env (see the +//! `drift_check_action_*` unit tests). Mirrors wardline's `_live_oracle.py` +//! §5c SKIP→FAILURE primitive. +//! +//! ── Scope / honesty caveats ── +//! +//! * Parse-surface, not envelope-surface. `parse_entity_associations_response`, +//! `EntityAssociation`, and `EntityAssociationsResponse` are `pub` (reachable +//! from this external test crate via the `pub use loomweave_federation::filigree::*` +//! re-export), so this oracle drives them directly. The downstream reverse-join +//! ENVELOPE shape (`matched`/`drifted`/`not_found`/`result_kind`, built by the +//! crate-PRIVATE `IssuesForAccumulator::{add_response,into_envelope}` + +//! `association_json`) is NOT reachable from `tests/` and is NOT re-proven here. +//! It is already pinned end-to-end in-crate by the `tool_issues_for` integration +//! tests in `crates/loomweave-mcp/tests/storage_tools.rs` (e.g. +//! `issues_for_includes_contained_entities_and_flags_drift`, +//! `issues_for_reports_resolved_endpoint_and_result_kind`). This oracle proves +//! the precondition those tests assume: the real parser yields PRECISELY the +//! per-row inputs the reverse-join joins on. +//! +//! * Layer 2 is fixture-to-fixture, not producer-source. It byte-compares the +//! vendored copy against Filigree's vendored fixture FILE — it does NOT +//! re-invoke Filigree's Python route handler from Rust. It therefore catches +//! vendored-vs-authority drift, not authority-vs-real-producer drift. The +//! authority-vs-real-producer gap is now closed on Filigree's OWN side: it +//! ships a real producer-source wire oracle +//! (`filigree/tests/federation/test_entity_associations_wire_conformance_oracle.py`, +//! `test_real_handler_produces_golden_row_shape`) that drives the live +//! `GET /api/entity-associations` route over its ASGI app with self-owned seeds +//! and ties the produced per-row key shape + derived enrichment to the golden. +//! The only honest limitation HERE is that this Rust test cannot invoke that +//! Python oracle, so Layer-2 from this side stays fixture-to-fixture by design; +//! the producer-source assurance lives in (and is owned by) Filigree's suite. + +use std::path::PathBuf; + +use loomweave_mcp::filigree::{EntityAssociation, parse_entity_associations_response}; +use serde_json::Value; + +/// The Filigree authority golden, vendored BYTE-IDENTICAL from +/// `filigree/tests/fixtures/contracts/entity-associations-response.json` +/// (confirmed via `cmp`). `include_str!` embeds the exact on-disk bytes. +const GOLDEN: &str = + include_str!("../../../docs/federation/fixtures/filigree-entity-associations-response.json"); + +/// Layer-1 byte-pin: lowercase-hex `blake3` of the vendored golden's exact +/// bytes. Pins the fixture so a silent edit/re-vendor reds here. +/// +/// Tamper proof: perturbing one hex char of this const (or one byte of the +/// fixture) makes `golden_bytes_match_layer1_pin` fail with a `left != right` +/// mismatch — the pin is load-bearing, not decorative. The +/// `golden_bytes_match_layer1_pin_rejects_a_mutated_byte` test additionally +/// demonstrates that a single mutated input byte produces a DIFFERENT digest. +const GOLDEN_BLAKE3: &str = "9234531b1ec3ff5ee1edc77a0a6e2b61e367d9d96d5c8e9b4e99f5b4a47cf6d4"; + +/// The single canonical example name in the fixture's `examples` array. +const EXAMPLE_NAME: &str = "live_v27_reverse_lookup_200"; + +/// The opaque Loomweave entity binding the canonical row carries — an SEI token. +/// Filigree stores this VERBATIM and never parses its grammar. +const OPAQUE_SEI_BINDING: &str = "loomweave:eid:0123456789abcdef0123456789abcdef"; + +/// Pull the `response.body` of the named example straight out of the vendored +/// golden, re-serialized to the bytes the producer puts on the wire. This is the +/// exact body Filigree's route returns; we feed it to the REAL consumer parser. +fn golden_response_body(example_name: &str) -> String { + let fixture: Value = + serde_json::from_str(GOLDEN).expect("vendored entity-associations golden parses as JSON"); + let body = fixture + .get("examples") + .and_then(Value::as_array) + .and_then(|examples| { + examples.iter().find(|example| { + example.get("name").and_then(Value::as_str) == Some(example_name) + }) + }) + .and_then(|example| example.pointer("/response/body")) + .unwrap_or_else(|| panic!("missing fixture example body {example_name}")) + .clone(); + serde_json::to_string(&body).expect("re-serialize golden response body") +} + +// ── Layer 1 — byte-pin ─────────────────────────────────────────────────────── + +#[test] +fn golden_bytes_match_layer1_pin() { + let actual = blake3::hash(GOLDEN.as_bytes()).to_hex().to_string(); + assert_eq!( + actual, GOLDEN_BLAKE3, + "vendored filigree entity-associations golden drifted from its byte-pin; \ + re-vendor BYTE-IDENTICAL from filigree and update GOLDEN_BLAKE3" + ); +} + +#[test] +fn golden_bytes_match_layer1_pin_rejects_a_mutated_byte() { + // Tamper proof: flipping one byte of the vendored golden produces a digest + // that does NOT equal the pin. This demonstrates the Layer-1 assertion is + // load-bearing — it would catch a silent single-byte edit of the fixture. + let mut tampered = GOLDEN.as_bytes().to_vec(); + tampered[0] ^= 0x01; + let mutated = blake3::hash(&tampered).to_hex().to_string(); + assert_ne!( + mutated, GOLDEN_BLAKE3, + "a single mutated byte must NOT collide with the pinned digest" + ); +} + +// ── NON-CIRCULAR consumer oracle ───────────────────────────────────────────── + +#[test] +fn real_parser_accepts_the_golden_and_round_trips_the_opaque_binding() { + // Drive Loomweave's REAL parser on the producer's wire body. + let body = golden_response_body(EXAMPLE_NAME); + let parsed = parse_entity_associations_response(&body) + .expect("Loomweave must accept Filigree's canonical entity-associations body"); + + // The envelope's `associations` array deserialized to exactly one row — the + // canonical g15-oracle binding. + assert_eq!( + parsed.associations.len(), + 1, + "golden carries exactly one association row" + ); + let row: &EntityAssociation = &parsed.associations[0]; + + // (1) OPACITY — the opaque Loomweave entity ID lands in `loomweave_entity_id` + // BYTE-VERBATIM. The parser must NOT split/normalize the SEI grammar; + // it is an opaque join key (Filigree stored it opaquely, Loomweave + // resolves it via its alias map downstream). + assert_eq!( + row.loomweave_entity_id, OPAQUE_SEI_BINDING, + "the opaque SEI binding must round-trip verbatim into loomweave_entity_id" + ); + + // (2) REVERSE-JOIN INPUTS — the parsed row carries exactly the three fields + // the `issues_for` reverse-join consumes (read off the REAL parser's + // output, asserted against the producer's source-of-truth literals): + // * issue_id → global dedup key (`seen_issue_ids`) + // * loomweave_entity_id → alias→current-entity join key + // * content_hash_at_attach → drift classification (matched vs drifted) + assert_eq!( + row.issue_id, "test-045076e30f", + "issue_id (reverse-join dedup key) must parse" + ); + assert_eq!( + row.content_hash_at_attach, "hash-g15-oracle", + "content_hash_at_attach (drift-classification key) must parse" + ); + + // (3) DISPLAY ENRICHMENT — the optional display-only fields parse when present + // (they `default` to empty when the producer omits them). + assert_eq!(row.attached_at, "2026-06-13T00:00:00+00:00"); + assert_eq!(row.attached_by, "g15-oracle"); +} + +#[test] +fn real_parser_ignores_the_unmodelled_producer_fields() { + // The v2/v3 warpline-seam fields (claimed_at, closed_at, claim_commit, + // close_commit, status, status_category, orphan_status, signature, …) and the + // co-emitted canonical `entity_id` are present in the golden body but NOT + // modelled by `EntityAssociation`. The consumer must IGNORE them additively + // (no `deny_unknown_fields`), so a v1/v2 consumer still parses a v3 body. + let body = golden_response_body(EXAMPLE_NAME); + + // Sanity: the golden body really does carry those unmodelled keys (otherwise + // this test would pass vacuously). + let body_value: Value = serde_json::from_str(&body).expect("body parses"); + let first_row = &body_value["associations"][0]; + for unmodelled in [ + "entity_id", + "entity_kind", + "orphan_status", + "freshness_status", + "claimed_at", + "closed_at", + "claim_commit", + "close_commit", + "status", + "status_category", + ] { + assert!( + first_row.get(unmodelled).is_some(), + "golden row must carry the unmodelled producer field {unmodelled} \ + (else the ignore-unknown assertion is vacuous)" + ); + } + + // The REAL parser accepts the body despite those extra fields. + let parsed = parse_entity_associations_response(&body) + .expect("consumer must tolerate unmodelled additive producer fields"); + assert_eq!(parsed.associations.len(), 1); +} + +#[test] +fn real_parser_tolerates_the_prev26_clarion_entity_id_alias() { + // Live parse behaviour the consumer must preserve: a pre-v26 producer (or a + // JSONL export) emits `clarion_entity_id` instead of `loomweave_entity_id`. + // The `#[serde(alias = "clarion_entity_id")]` on the field must map it onto + // `loomweave_entity_id`. Drive the REAL parser to prove the alias is wired. + let legacy_body = r#"{"associations":[{ + "issue_id":"filigree-legacy", + "clarion_entity_id":"loomweave:eid:0123456789abcdef0123456789abcdef", + "content_hash_at_attach":"hash-legacy" + }]}"#; + let parsed = parse_entity_associations_response(legacy_body) + .expect("consumer must tolerate the pre-v26 clarion_entity_id field name"); + assert_eq!(parsed.associations.len(), 1); + assert_eq!( + parsed.associations[0].loomweave_entity_id, OPAQUE_SEI_BINDING, + "the pre-v26 clarion_entity_id alias must deserialize into loomweave_entity_id" + ); +} + +#[test] +fn real_parser_degrades_absent_associations_to_empty_list() { + // Enrich-only degrade: an empty/absent envelope key yields an empty list + // (`#[serde(default)]`), NOT a hard `missing field associations` failure — + // the reverse-join then reports `no_matches`, never an error. Drive the REAL + // parser to prove the `default` is wired. + let parsed = parse_entity_associations_response("{}") + .expect("an empty envelope must degrade to an empty association list"); + assert!( + parsed.associations.is_empty(), + "absent associations key degrades to an empty list (enrich-only)" + ); +} + +// ── Layer 2 — drift recheck vs the Filigree producer source of truth ───────── + +/// Env var that ARMS the cross-repo drift recheck: when set truthy +/// (`1`/`true`/`yes`/`on`), an absent sibling Filigree repo becomes a hard +/// FAILURE instead of a skip-clean. A release-gate CI job sets this so a release +/// cut can never silently ship with the authority recheck un-run. Mirrors +/// wardline's `WARDLINE_LIVE_ORACLE_REQUIRED` SKIP→FAILURE primitive. +const DRIFT_REQUIRED_ENV: &str = "LOOMWEAVE_DRIFT_REQUIRED"; + +/// The action the Layer-2 recheck must take, given (a) whether the recheck is +/// armed as REQUIRED and (b) whether the sibling Filigree authority fixture is +/// present. Pure + total over the 2×2 so all three outcomes are unit-testable +/// without touching process-global env (which `cargo`'s parallel test threads +/// share — mutating it would race the real recheck) or the filesystem. +#[derive(Debug, PartialEq, Eq)] +enum DriftCheck { + /// Sibling present — byte-compare the vendored golden against the authority. + Compare, + /// Sibling absent and the recheck is NOT armed — skip cleanly (CI / detached + /// checkout). Layer-1 byte-pin + the vendored copy still gate the run. + SkipClean, + /// Sibling absent but the recheck IS armed (`LOOMWEAVE_DRIFT_REQUIRED=1`) — + /// fail: a release-gate run must not skip the cross-repo authority check. + FailRequired, +} + +fn drift_check_action(required: bool, authority_exists: bool) -> DriftCheck { + match (authority_exists, required) { + (true, _) => DriftCheck::Compare, + (false, false) => DriftCheck::SkipClean, + (false, true) => DriftCheck::FailRequired, + } +} + +fn drift_required() -> bool { + matches!( + std::env::var(DRIFT_REQUIRED_ENV) + .unwrap_or_default() + .trim() + .to_ascii_lowercase() + .as_str(), + "1" | "true" | "yes" | "on" + ) +} + +#[test] +fn vendored_golden_matches_filigree_authority() { + let repo = std::env::var("FILIGREE_REPO").unwrap_or_else(|_| "/home/john/filigree".to_owned()); + let authority: PathBuf = PathBuf::from(repo) + .join("tests") + .join("fixtures") + .join("contracts") + .join("entity-associations-response.json"); + + match drift_check_action(drift_required(), authority.exists()) { + DriftCheck::SkipClean => { + // Sibling Filigree repo absent (CI / detached checkout) and the recheck + // is NOT armed. The vendored copy + Layer-1 pin still hold; we just + // can't recheck against the upstream producer source here. + eprintln!( + "filigree authority fixture not found at {} — skipping Layer-2 drift recheck \ + (set FILIGREE_REPO to enable, or {DRIFT_REQUIRED_ENV}=1 to make absence a failure)", + authority.display() + ); + } + DriftCheck::FailRequired => { + // Armed for a release gate: an absent sibling is a HARD failure, not a + // silent skip, so the cross-repo authority check is never un-run at a + // release cut. + panic!( + "filigree authority fixture not found at {} but {DRIFT_REQUIRED_ENV} is set — \ + the cross-repo drift recheck is REQUIRED; make the sibling Filigree repo \ + available (FILIGREE_REPO) or unset {DRIFT_REQUIRED_ENV}", + authority.display() + ); + } + DriftCheck::Compare => { + let authority_bytes = + std::fs::read(&authority).expect("read filigree authority fixture"); + assert_eq!( + authority_bytes, + GOLDEN.as_bytes(), + "vendored golden has DRIFTED from the Filigree authority at {} (filigree is the \ + PRODUCER — its copy is the authority); re-vendor BYTE-IDENTICAL", + authority.display() + ); + } + } +} + +#[test] +fn drift_check_action_compares_when_authority_present() { + // Sibling present → byte-compare, regardless of the arming flag. + assert_eq!(drift_check_action(false, true), DriftCheck::Compare); + assert_eq!(drift_check_action(true, true), DriftCheck::Compare); +} + +#[test] +fn drift_check_action_skips_clean_when_absent_and_unarmed() { + // Sibling absent and recheck NOT armed → skip cleanly (default CI posture). + assert_eq!(drift_check_action(false, false), DriftCheck::SkipClean); +} + +#[test] +fn drift_check_action_fails_when_absent_but_required() { + // Sibling absent but recheck ARMED → hard failure (release-gate posture). This + // is the load-bearing arming the MEDIUM finding required: an absent-sibling run + // under LOOMWEAVE_DRIFT_REQUIRED no longer shows green. + assert_eq!(drift_check_action(true, false), DriftCheck::FailRequired); +} diff --git a/docs/federation/fixtures/filigree-entity-associations-response.json b/docs/federation/fixtures/filigree-entity-associations-response.json index 54dbf34..387979a 100644 --- a/docs/federation/fixtures/filigree-entity-associations-response.json +++ b/docs/federation/fixtures/filigree-entity-associations-response.json @@ -2,7 +2,7 @@ "_meta": { "contract": "filigree-entity-associations-response", "endpoint": "GET /api/entity-associations?entity_id={entity_id}", - "fixture_version": 1, + "fixture_version": 3, "stability": "normative", "authority": "Weft G15 / ADR-029 EntityAssociation producer-consumer conformance", "producer": "Filigree dashboard_routes.entities.api_list_associations_by_entity", @@ -12,8 +12,8 @@ "loomweave/docs/federation/fixtures/filigree-entity-associations-response.json" ], "verification": "Filigree: uv run pytest tests/api/test_entity_associations.py -q; Loomweave: cargo test -p loomweave-federation filigree::tests::parses_canonical_filigree_entity_association_fixture", - "updated": "2026-06-13", - "description": "Canonical live Filigree reverse-lookup response for EntityAssociation rows. The producer test emits this body through Filigree's HTTP route; the consumer test deserializes the same body in Loomweave." + "updated": "2026-06-24", + "description": "Canonical live Filigree reverse-lookup response for EntityAssociation rows. The producer test emits this body through Filigree's HTTP route; the consumer test deserializes the same body in Loomweave. v2 (warpline seam): each row carries the bound issue's lifecycle facts (claimed_at, closed_at, status, status_category) for warpline's 'changed since claimed/closed' correlation; all four are null for an orphaned binding. v3 (warpline commit-anchor seam, contract B): each row also carries the opaque per-issue commit anchors (claim_commit, close_commit) — the caller-supplied branch@sha the issue was claimed/closed at, stored verbatim and never parsed by Filigree; null for an orphaned binding or when no commit was supplied (warpline then falls back to the timestamp). Additive — Loomweave's consumer ignores unknown fields (no serde deny_unknown_fields), so a v1/v2 consumer still parses v3. The loomweave repo-local copy should be synced for documentation parity (non-breaking)." }, "shape_decl": { "kind": "filigree-entity-associations-fixture-shapes", @@ -54,7 +54,13 @@ "freshness_status": "unknown", "signature": null, "signoff_seq": null, - "signed_content_hash": null + "signed_content_hash": null, + "claimed_at": null, + "closed_at": null, + "claim_commit": null, + "close_commit": null, + "status": "open", + "status_category": "open" } ] } From e48e0cd0853e43e4ec11a6b9e182009497be0fa1 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:43:16 +1000 Subject: [PATCH 4/6] test(federation): conformance oracles for 3 loomweave<->filigree seams - capabilities PRODUCER: Layer-1 blake3 byte-pin of the /api/v1/_capabilities golden (the live-handler recheck via the real serve binary already existed) - loomweave-scan-results PRODUCER: freeze the emitted ScanResultsRequest body (authored from the real prepare_batch) + byte-pin + non-circular recheck - issue-detail CONSUMER: drives the real IssueDetail parse on filigree's golden (id<-issue_id alias, title/status/priority non-default -> drop/retype reds) All byte-pinned, goldens byte-identical to filigree, Layer-2 drift recheck. loomweave oracles: 5 + 14 + 8 passed (cargo). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/loomweave-cli/tests/serve.rs | 46 ++ .../scan_results_wire_conformance_oracle.rs | 358 +++++++++++++++ .../tests/issue_detail_conformance_oracle.rs | 419 ++++++++++++++++++ .../fixtures/filigree-issues-get.json | 85 ++++ .../loomweave-scan-results-wire.golden.json | 73 +++ 5 files changed, 981 insertions(+) create mode 100644 crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs create mode 100644 crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs create mode 100644 docs/federation/fixtures/filigree-issues-get.json create mode 100644 docs/federation/fixtures/loomweave-scan-results-wire.golden.json diff --git a/crates/loomweave-cli/tests/serve.rs b/crates/loomweave-cli/tests/serve.rs index 3c94629..24f41aa 100644 --- a/crates/loomweave-cli/tests/serve.rs +++ b/crates/loomweave-cli/tests/serve.rs @@ -823,6 +823,52 @@ fn serve_http_files_storage_failure_returns_closed_error_without_raw_detail() { assert!(!body.contains(&dir.path().display().to_string())); } +/// The `GET /api/v1/_capabilities` federation golden, embedded BYTE-IDENTICAL +/// from the on-disk normative fixture. `include_str!` captures the exact bytes +/// (meta + shape_decl + examples), so this const IS the wire authority the +/// producer-recheck tests (`serve_http_responses_match_federation_fixture_contracts` +/// and `serve_http_capabilities_and_mcp_stdio_coexist`) compare the live +/// `get_capabilities` handler against. +const CAPABILITIES_GOLDEN: &str = + include_str!("../../../docs/federation/fixtures/get-api-v1-capabilities.json"); + +/// Layer-1 byte-pin: lowercase-hex `blake3` over the EXACT bytes of the +/// capabilities golden. Pins the whole fixture file so a silent reformat, +/// field reorder, or re-vendor reds here even when the semantic shape the +/// producer-recheck validates is unchanged. +/// +/// Tamper proof: perturbing one byte of the fixture (or one hex char of this +/// const) makes `capabilities_golden_bytes_match_layer1_pin` fail with a +/// `left != right` mismatch; `capabilities_golden_byte_pin_rejects_a_mutated_byte` +/// additionally proves a single flipped input byte yields a DIFFERENT digest. +const CAPABILITIES_GOLDEN_BLAKE3: &str = + "74d5fd1230a62f7a279e54d2a798ce85b3ae8b962f593d1d6bd8c0e2db70dbf7"; + +#[test] +fn capabilities_golden_bytes_match_layer1_pin() { + let actual = blake3::hash(CAPABILITIES_GOLDEN.as_bytes()) + .to_hex() + .to_string(); + assert_eq!( + actual, CAPABILITIES_GOLDEN_BLAKE3, + "vendored get-api-v1-capabilities golden drifted from its byte-pin; \ + re-vendor BYTE-IDENTICAL and update CAPABILITIES_GOLDEN_BLAKE3" + ); +} + +#[test] +fn capabilities_golden_byte_pin_rejects_a_mutated_byte() { + // Tamper proof: flipping one byte of the golden produces a digest that + // does NOT match the pin, demonstrating the byte-pin is load-bearing. + let mut tampered = CAPABILITIES_GOLDEN.as_bytes().to_vec(); + tampered[0] ^= 0x01; + let mutated = blake3::hash(&tampered).to_hex().to_string(); + assert_ne!( + mutated, CAPABILITIES_GOLDEN_BLAKE3, + "a single mutated byte must NOT collide with the pinned digest" + ); +} + #[test] fn serve_http_capabilities_and_mcp_stdio_coexist() { let dir = tempfile::tempdir().expect("temp project"); diff --git a/crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs b/crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs new file mode 100644 index 0000000..8fac9e3 --- /dev/null +++ b/crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs @@ -0,0 +1,358 @@ +//! Loomweave → Filigree scan-results (`POST /api/v1/scan-results`) PRODUCER wire +//! conformance oracle. +//! +//! The PRODUCER side of the cross-repo `scan-results-lw` Loomweave→Filigree seam. +//! LOOMWEAVE (this crate) is the PRODUCER: `loomweave analyze` Phase 8 maps its +//! persisted findings onto Filigree's intake schema via +//! `loomweave_federation::scan_results::prepare_batch` / `wire_finding` +//! (`scan_source="loomweave"`) and POSTs the `ScanResultsRequest` body to +//! `POST /api/v1/scan-results`. Filigree is the CONSUMER. +//! +//! This module FREEZES Loomweave's produced wire (the assembled multi-finding +//! `ScanResultsRequest` body, for FIXED `FindingForEmit` inputs) to a committed +//! golden, plus a NON-CIRCULAR producer-source recheck that re-invokes the REAL +//! `prepare_batch` on those fixed inputs and asserts the produced body ties to the +//! golden. The golden lives in the federation authority dir +//! (`docs/federation/fixtures/loomweave-scan-results-wire.golden.json`), where +//! Filigree would later vendor a byte-identical consumer copy — mirroring the +//! entity-associations and wardline-taint-fact goldens. +//! +//! Mirrors the proven layering of the entity-associations / wardline-taint-fact +//! oracles, in the PRODUCER direction (no upstream to drift-check against — +//! Loomweave is the authority for this body): +//! +//! * Layer 1 — a byte-pin: a `blake3` digest over the committed golden bytes, +//! asserted against a const. A single-byte edit/re-freeze of the golden reds +//! here. (Proven to red on tamper — see +//! `golden_bytes_match_layer1_pin_rejects_a_mutated_byte`.) +//! +//! * A NON-CIRCULAR producer-source recheck: re-invoke the REAL `prepare_batch` +//! on the fixed inputs, serialize its `request` to a `serde_json::Value`, and +//! assert it equals the golden parsed to a `Value` (semantic, key-order-immune +//! compare). The body is built by REAL producer code, NOT restated against +//! itself. +//! +//! * The DISTINGUISHING contract this oracle adds over the in-module unit tests: +//! it pins the ASSEMBLED multi-finding body AND affirmatively asserts the +//! ABSENCE of wardline's per-finding `fingerprint` / `fingerprint_scheme` and +//! the request-level `scanned_paths` (Loomweave's wire deliberately omits all +//! three — Filigree computes the dedup fingerprint server-side). A regression +//! that started emitting wardline-style fields would red here. +//! +//! ── Scope / honesty caveats ── +//! +//! * PRODUCER-ONLY. This proves Loomweave produces a STABLE, FROZEN wire. There is +//! no Filigree consumer oracle for THIS fingerprint-less body — Filigree's +//! `tests/federation/test_scan_results_wire_conformance_oracle.py` is +//! wardline-specific (its golden carries `fingerprints` / `scanned_paths`). So +//! cross-repo INGESTION of this exact shape is not proven by this oracle; only +//! that Loomweave emits it deterministically. +//! +//! * SEMANTIC wire, not the literal reqwest byte stream. The recheck compares +//! `serde_json::Value`s, so it freezes the produced wire's SEMANTIC shape +//! (fields + values + structure), key-order-immune. The actual on-wire bytes are +//! the compact `reqwest .json()` (`serde_json::to_vec`) encoding of the same +//! struct; this oracle does not byte-compare that compact stream. + +use loomweave_federation::scan_results::{ + EmitOptions, FindingForEmit, LOOMWEAVE_SCAN_SOURCE, PreparedBatch, prepare_batch, +}; +use serde_json::Value; + +/// The committed Loomweave authority golden: the frozen `ScanResultsRequest` wire +/// body for the fixed inputs in [`fixed_rows`] / [`fixed_opts`]. Loomweave is the +/// PRODUCER/authority for this body. `include_str!` embeds the exact on-disk bytes +/// (the `../../../` math reaches the workspace-root `docs/` dir, matching the +/// entity-associations oracle). +const GOLDEN: &str = + include_str!("../../../docs/federation/fixtures/loomweave-scan-results-wire.golden.json"); + +/// Layer-1 byte-pin: lowercase-hex `blake3` of the committed golden's exact bytes. +/// Pins the fixture so a silent edit/re-freeze reds here. +/// +/// Tamper proof: perturbing one hex char of this const (or one byte of the golden +/// file) makes `golden_bytes_match_layer1_pin` fail with a `left != right` +/// mismatch — the pin is load-bearing, not decorative. +const GOLDEN_BLAKE3: &str = "35eb440fae37afdcb3ea3ca04829a32eb587b2665d9d0694520137fa2e9be023"; + +// ── Fixed producer inputs (owned by this oracle) ───────────────────────────── + +/// The fixed `FindingForEmit` rows the golden freezes. Three rows chosen to +/// exercise the structural variants of Loomweave's produced wire: +/// +/// * a defect (WARN→`medium`, confidence + basis, real path + lines, nested +/// `metadata.loomweave.*` with `related`/`supports`/`supported_by`); +/// * a fact (NONE→`info`, no confidence/basis); +/// * a synthetic-anchor finding (subsystem entity, no `source_file_path`) that +/// emits against the `default_path` fallback, flagged `synthetic_anchor=true` +/// with NO line numbers. +fn fixed_rows() -> Vec { + vec![ + FindingForEmit { + id: "core:finding:circular".to_owned(), + rule_id: "LMWV-PY-STRUCTURE-001".to_owned(), + kind: "defect".to_owned(), + severity: "WARN".to_owned(), + confidence: Some(0.95), + confidence_basis: Some("ast_match".to_owned()), + message: "Circular import detected".to_owned(), + entity_id: "python:class:auth.tokens::TokenManager".to_owned(), + related_entities_json: r#"["python:class:auth.sessions::SessionStore"]"#.to_owned(), + supports_json: r#"["python:function:auth.tokens::mint"]"#.to_owned(), + supported_by_json: "[]".to_owned(), + source_file_path: Some("src/auth/tokens.py".to_owned()), + source_line_start: Some(12), + source_line_end: Some(20), + }, + FindingForEmit { + id: "core:finding:entrypoint-fact".to_owned(), + rule_id: "LMWV-PY-FACT-ENTRYPOINT".to_owned(), + kind: "fact".to_owned(), + severity: "NONE".to_owned(), + confidence: None, + confidence_basis: None, + message: "HTTP route entry point".to_owned(), + entity_id: "python:function:api.routes::handle_request".to_owned(), + related_entities_json: "[]".to_owned(), + supports_json: "[]".to_owned(), + supported_by_json: "[]".to_owned(), + source_file_path: Some("src/api/routes.py".to_owned()), + source_line_start: Some(42), + source_line_end: Some(58), + }, + FindingForEmit { + id: "core:finding:weak-modularity".to_owned(), + rule_id: "LMWV-SUBSYSTEM-COHESION".to_owned(), + kind: "defect".to_owned(), + severity: "ERROR".to_owned(), + confidence: Some(0.5), + confidence_basis: Some("coupling_metric".to_owned()), + message: "Subsystem exhibits weak modularity".to_owned(), + entity_id: "core:subsystem:abcd1234".to_owned(), + related_entities_json: "[]".to_owned(), + supports_json: "[]".to_owned(), + supported_by_json: "[]".to_owned(), + source_file_path: None, + source_line_start: None, + source_line_end: None, + }, + ] +} + +/// The fixed emit options the golden freezes: a normal full final batch with a +/// known `scan_run_id` and a `default_path` so the synthetic-anchor row emits +/// (rather than being skipped). +fn fixed_opts() -> EmitOptions { + EmitOptions { + scan_run_id: Some("run-conformance-1".to_owned()), + mark_unseen: true, + complete_scan_run: true, + default_path: Some("/repo/root".to_owned()), + } +} + +/// Re-invoke the REAL producer on the fixed inputs. The single source of the +/// produced wire — both the golden (authored from this) and the recheck drive it. +fn produced_batch() -> PreparedBatch { + prepare_batch(&fixed_rows(), &fixed_opts()) +} + +/// The produced `ScanResultsRequest` body as a `serde_json::Value`, exactly as the +/// HTTP client serializes it (`reqwest .json()` → `serde_json::to_*` over the same +/// `Serialize` impl). +fn produced_wire_value() -> Value { + serde_json::to_value(&produced_batch().request).expect("serialize produced scan-results request") +} + +fn golden_value() -> Value { + serde_json::from_str(GOLDEN).expect("committed scan-results golden parses as JSON") +} + +// ── Layer 1 — byte-pin ─────────────────────────────────────────────────────── + +#[test] +fn golden_bytes_match_layer1_pin() { + let actual = blake3::hash(GOLDEN.as_bytes()).to_hex().to_string(); + assert_eq!( + actual, GOLDEN_BLAKE3, + "committed loomweave scan-results golden drifted from its byte-pin; \ + re-freeze from the producer and update GOLDEN_BLAKE3" + ); +} + +#[test] +fn golden_bytes_match_layer1_pin_rejects_a_mutated_byte() { + // Tamper proof: flipping one byte of the committed golden produces a digest + // that does NOT equal the pin. This demonstrates the Layer-1 assertion is + // load-bearing — it would catch a silent single-byte edit of the fixture. + let mut tampered = GOLDEN.as_bytes().to_vec(); + tampered[0] ^= 0x01; + let mutated = blake3::hash(&tampered).to_hex().to_string(); + assert_ne!( + mutated, GOLDEN_BLAKE3, + "a single mutated byte must NOT collide with the pinned digest" + ); +} + +// ── NON-CIRCULAR producer-source recheck ───────────────────────────────────── + +#[test] +fn real_producer_reproduces_the_golden_wire() { + // Re-invoke the REAL `prepare_batch` on the fixed inputs and assert the + // produced wire body equals the committed golden — semantic (key-order-immune) + // Value compare. NON-CIRCULAR: the body is assembled by real producer code; the + // golden is the frozen expectation, not the producer echoed at itself. + assert_eq!( + produced_wire_value(), + golden_value(), + "the real producer (prepare_batch) no longer reproduces the committed \ + scan-results golden; if this change is intended, re-freeze the golden and \ + update GOLDEN_BLAKE3" + ); +} + +#[test] +fn producer_emits_the_loomweave_positive_contract() { + // Affirmative positive-contract assertions read off the REAL producer's output + // (not the golden against itself): the request-level knobs and the per-finding + // severity mapping / nested loomweave axes / synthetic-anchor flagging that + // define Loomweave's wire. + let wire = produced_wire_value(); + + // Request-level knobs. + assert_eq!(wire["scan_source"], Value::from(LOOMWEAVE_SCAN_SOURCE)); + assert_eq!(wire["scan_source"], Value::from("loomweave")); + assert_eq!(wire["scan_run_id"], Value::from("run-conformance-1")); + assert_eq!(wire["mark_unseen"], Value::Bool(true)); + assert_eq!( + wire["create_observations"], + Value::Bool(false), + "Loomweave emits findings, never observations" + ); + assert_eq!(wire["complete_scan_run"], Value::Bool(true)); + + let findings = wire["findings"].as_array().expect("findings array"); + assert_eq!(findings.len(), 3, "all three fixed rows emit (none skipped)"); + + // Row 0 — defect: WARN→medium, real path + lines, nested loomweave.* round-trip. + let defect = &findings[0]; + assert_eq!(defect["path"], Value::from("src/auth/tokens.py")); + assert_eq!(defect["rule_id"], Value::from("LMWV-PY-STRUCTURE-001")); + assert_eq!(defect["severity"], Value::from("medium"), "WARN→medium"); + assert_eq!(defect["line_start"], Value::from(12)); + assert_eq!(defect["line_end"], Value::from(20)); + assert_eq!(defect["metadata"]["kind"], Value::from("defect")); + assert_eq!(defect["metadata"]["confidence"], Value::from(0.95)); + assert_eq!(defect["metadata"]["confidence_basis"], Value::from("ast_match")); + let lw = &defect["metadata"]["loomweave"]; + assert_eq!( + lw["entity_id"], + Value::from("python:class:auth.tokens::TokenManager") + ); + assert_eq!( + lw["related_entities"], + serde_json::json!(["python:class:auth.sessions::SessionStore"]) + ); + assert_eq!( + lw["supports"], + serde_json::json!(["python:function:auth.tokens::mint"]) + ); + assert_eq!(lw["supported_by"], serde_json::json!([])); + assert_eq!( + lw["internal_severity"], + Value::from("WARN"), + "internal vocabulary round-trips under loomweave.*" + ); + assert_eq!(lw["internal_status"], Value::from("open")); + + // Row 1 — fact: NONE→info, no confidence/basis. + let fact = &findings[1]; + assert_eq!(fact["severity"], Value::from("info"), "NONE→info"); + assert_eq!(fact["metadata"]["kind"], Value::from("fact")); + assert!( + fact["metadata"].get("confidence").is_none(), + "fact omits confidence: {fact}" + ); + assert!( + fact["metadata"].get("confidence_basis").is_none(), + "fact omits confidence_basis: {fact}" + ); + assert_eq!( + fact["metadata"]["loomweave"]["internal_severity"], + Value::from("NONE") + ); + + // Row 2 — synthetic anchor: emits against default_path, flagged, no lines. + let synthetic = &findings[2]; + assert_eq!(synthetic["path"], Value::from("/repo/root")); + assert_eq!(synthetic["severity"], Value::from("high"), "ERROR→high"); + assert_eq!( + synthetic["metadata"]["loomweave"]["synthetic_anchor"], + Value::Bool(true) + ); + assert!( + synthetic.get("line_start").is_none() && synthetic.get("line_end").is_none(), + "synthetic anchor carries no line position: {synthetic}" + ); +} + +#[test] +fn producer_omits_wardlines_fingerprint_and_scanned_paths() { + // The DISTINGUISHING contract: Loomweave's wire is fingerprint-LESS. Unlike + // wardline's `POST /api/weft/scan-results` body (which carries a per-finding + // `fingerprint` + `fingerprint_scheme` and a request-level `scanned_paths`), + // Loomweave omits all three and relies on Filigree computing the dedup + // fingerprint server-side. A regression that started emitting wardline-style + // fields would red here. + let wire = produced_wire_value(); + + // Request-level: no `scanned_paths` (nor any of wardline's other request keys). + let request_obj = wire.as_object().expect("request is a JSON object"); + assert!( + !request_obj.contains_key("scanned_paths"), + "Loomweave's scan-results request must NOT carry wardline's scanned_paths: {wire}" + ); + + // The request carries EXACTLY Loomweave's known keys — no un-sanctioned extra + // (so a newly-added wardline-style top-level field reds here too). + let request_keys: std::collections::BTreeSet<&str> = + request_obj.keys().map(String::as_str).collect(); + let expected: std::collections::BTreeSet<&str> = [ + "scan_source", + "scan_run_id", + "mark_unseen", + "create_observations", + "complete_scan_run", + "findings", + ] + .into_iter() + .collect(); + assert_eq!( + request_keys, expected, + "Loomweave's scan-results request key set drifted (extra/missing top-level field)" + ); + + // Per-finding: no `fingerprint` / `fingerprint_scheme` on ANY finding, at the + // top level OR nested under metadata / metadata.loomweave. + for (i, finding) in wire["findings"] + .as_array() + .expect("findings array") + .iter() + .enumerate() + { + for banned in ["fingerprint", "fingerprint_scheme"] { + assert!( + finding.get(banned).is_none(), + "finding[{i}] must NOT carry wardline's top-level {banned}: {finding}" + ); + assert!( + finding["metadata"].get(banned).is_none(), + "finding[{i}].metadata must NOT carry wardline's {banned}: {finding}" + ); + assert!( + finding["metadata"]["loomweave"].get(banned).is_none(), + "finding[{i}].metadata.loomweave must NOT carry wardline's {banned}: {finding}" + ); + } + } +} diff --git a/crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs b/crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs new file mode 100644 index 0000000..3c38451 --- /dev/null +++ b/crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs @@ -0,0 +1,419 @@ +//! Filigree → Loomweave issue-detail wire conformance oracle. +//! +//! The CONSUMER side of the cross-repo `filigree<->loomweave` issue-detail seam. +//! FILIGREE is the PRODUCER/authority of the `GET /api/weft/issues/{issue_id}` +//! response (weft generation, ADR-002 / Phase C3): the route handler +//! `api_weft_get_issue` projects a stored issue through `issue_to_weft` into the +//! `IssueWeft` shape, renaming the entity's OWN primary key `id` → `issue_id` +//! while reference fields keep their classic names. LOOMWEAVE (this crate) is the +//! CONSUMER: its `issues_for` reverse-join enriches a matched association row by +//! deserializing that body through +//! `loomweave_federation::filigree::parse_issue_detail_response` (re-exported as +//! `loomweave_mcp::filigree::parse_issue_detail_response`) into the four-field +//! `IssueDetail` stub. +//! +//! This oracle pins that the bytes Filigree produces are accepted + correctly +//! deserialized by Loomweave's REAL parse code path. Mirrors the proven layering +//! of `entity_associations_conformance_oracle.rs`: +//! +//! * Layer 1 — a byte-pin: a `blake3` digest over the vendored golden bytes, +//! asserted against a const. A single-byte drift reds the pin. (Proven to red +//! on tamper — see `golden_bytes_match_layer1_pin_rejects_a_mutated_byte`.) +//! +//! * A NON-CIRCULAR consumer oracle: the golden's 200 response body is fed +//! through Loomweave's REAL `parse_issue_detail_response`, and the parsed +//! `IssueDetail` is asserted off the REAL parser's output (NOT the golden +//! restated against itself). Crucially, the load-bearing property of THIS seam +//! is the OPPOSITE of the entity-associations seam: there the contract was +//! graceful degradation (defaults, ignore-unknown); HERE `title`, `status`, +//! and `priority` are REQUIRED (no `serde(default)`) and `priority` is `i64` — +//! dropping or retyping any of them MUST hard-fail. A positive parse alone +//! cannot prove that (required and default fields parse a full body +//! identically), so the core carries NEGATIVE cases: drop/retype each required +//! field via a mutated `Value` and assert the REAL parser returns `Err`. +//! +//! The `id` field is the one exception — its source carries +//! `#[serde(alias = "issue_id", default)]`, so `id` deserializes from the +//! route's `issue_id` AND is itself default. Dropping `issue_id` therefore +//! does NOT fail (it degrades to `""`); the alias is proven POSITIVELY +//! instead (golden carries `issue_id`, no `id`; parser yields the id). +//! +//! * Layer 2 — a drift recheck: the vendored fixture bytes are byte-compared +//! against the authority golden in the Filigree repo +//! (`$FILIGREE_REPO/tests/fixtures/contracts/weft/issues-get.json`, +//! `FILIGREE_REPO` defaulting to `/home/john/filigree`). Filigree is the +//! PRODUCER, so its copy is the authority. When the sibling repo is absent the +//! recheck is SKIP-CLEAN *by default* — Layer-1 + the vendored copy still gate +//! — but a release-gate job can ARM it via `LOOMWEAVE_DRIFT_REQUIRED=1` +//! (`1`/`true`/`yes`/`on`), turning the absent-sibling skip into a hard +//! FAILURE so a release cut never silently ships the authority check un-run. +//! The arming decision is a pure helper (`drift_check_action`) so all three +//! branches are exercised deterministically without mutating process-global +//! env. +//! +//! ── Scope / honesty caveats ── +//! +//! * Parse-surface, not envelope-surface. `parse_issue_detail_response` and +//! `IssueDetail` are `pub` (reachable from this external test crate via the +//! `pub use loomweave_federation::filigree::*` re-export), so this oracle drives +//! them directly. The downstream `issues_for` enrichment ENVELOPE (how an +//! `IssueDetail` is folded into a match row) is crate-private and is NOT +//! re-proven here; it is pinned in-crate by `storage_tools.rs`. This oracle +//! proves the precondition: the real parser yields PRECISELY the (id, title, +//! status, priority) tuple the enrichment consumes off the producer's wire. +//! +//! * Only the `live_v_issue_detail_200` example is fed to the parser. The +//! `error_not_found` example is a different (`error`/`code`) shape handled at the +//! HTTP-status layer (`get_json_or_none` → `None`), already covered in-crate by +//! `issue_detail_http_client_maps_404_to_none`; it is NOT parsed here. +//! +//! * Layer 2 is fixture-to-fixture, not producer-source. It byte-compares the +//! vendored copy against Filigree's vendored fixture FILE — it does NOT re-invoke +//! Filigree's Python route handler from Rust. The authority-vs-real-producer gap +//! is closed on Filigree's OWN side, by its producer-source wire oracle +//! (`filigree/tests/federation/test_weft_issue_detail_wire_conformance_oracle.py`, +//! `test_real_handler_produces_golden_issue_detail_shape`), which drives the live +//! `GET /api/weft/issues/{issue_id}` route over its ASGI app with self-owned +//! seeds and ties the produced key shape to the golden. This Rust test cannot +//! invoke that Python oracle, so Layer-2 from this side stays fixture-to-fixture +//! by design; the producer-source assurance lives in (and is owned by) Filigree. + +use std::path::PathBuf; + +use loomweave_mcp::filigree::{IssueDetail, parse_issue_detail_response}; +use serde_json::Value; + +/// The Filigree authority golden, vendored BYTE-IDENTICAL from +/// `filigree/tests/fixtures/contracts/weft/issues-get.json` (confirmed via `cmp`). +/// `include_str!` embeds the exact on-disk bytes. +const GOLDEN: &str = include_str!("../../../docs/federation/fixtures/filigree-issues-get.json"); + +/// Layer-1 byte-pin: lowercase-hex `blake3` of the vendored golden's exact bytes. +/// Pins the fixture so a silent edit/re-vendor reds here. +/// +/// Tamper proof: perturbing one hex char of this const (or one byte of the +/// fixture) makes `golden_bytes_match_layer1_pin` fail with a `left != right` +/// mismatch — the pin is load-bearing, not decorative. The +/// `golden_bytes_match_layer1_pin_rejects_a_mutated_byte` test additionally +/// demonstrates that a single mutated input byte produces a DIFFERENT digest. +const GOLDEN_BLAKE3: &str = "94f2659a44f6e0a976fc6bc3f7957aee7f4bc613c07473ef40c9b0dba7134e03"; + +/// The 200-response example name in the fixture's `examples` array. The other +/// example (`error_not_found`) is a different shape handled at the HTTP layer and +/// is deliberately NOT fed to the parser here. +const EXAMPLE_NAME: &str = "live_v_issue_detail_200"; + +/// Pull the `response.body` of the named example straight out of the vendored +/// golden, re-serialized to the bytes the producer puts on the wire. This is the +/// exact body Filigree's route returns; we feed it to the REAL consumer parser. +fn golden_response_body(example_name: &str) -> Value { + let fixture: Value = + serde_json::from_str(GOLDEN).expect("vendored issue-detail golden parses as JSON"); + fixture + .get("examples") + .and_then(Value::as_array) + .and_then(|examples| { + examples + .iter() + .find(|example| example.get("name").and_then(Value::as_str) == Some(example_name)) + }) + .and_then(|example| example.pointer("/response/body")) + .unwrap_or_else(|| panic!("missing fixture example body {example_name}")) + .clone() +} + +fn golden_body_string(example_name: &str) -> String { + serde_json::to_string(&golden_response_body(example_name)).expect("re-serialize golden body") +} + +// ── Layer 1 — byte-pin ─────────────────────────────────────────────────────── + +#[test] +fn golden_bytes_match_layer1_pin() { + let actual = blake3::hash(GOLDEN.as_bytes()).to_hex().to_string(); + assert_eq!( + actual, GOLDEN_BLAKE3, + "vendored filigree issue-detail golden drifted from its byte-pin; \ + re-vendor BYTE-IDENTICAL from filigree and update GOLDEN_BLAKE3" + ); +} + +#[test] +fn golden_bytes_match_layer1_pin_rejects_a_mutated_byte() { + // Tamper proof: flipping one byte of the vendored golden produces a digest + // that does NOT equal the pin. This demonstrates the Layer-1 assertion is + // load-bearing — it would catch a silent single-byte edit of the fixture. + let mut tampered = GOLDEN.as_bytes().to_vec(); + tampered[0] ^= 0x01; + let mutated = blake3::hash(&tampered).to_hex().to_string(); + assert_ne!( + mutated, GOLDEN_BLAKE3, + "a single mutated byte must NOT collide with the pinned digest" + ); +} + +// ── NON-CIRCULAR consumer oracle ───────────────────────────────────────────── + +#[test] +fn real_parser_accepts_the_golden_and_extracts_the_modeled_tuple() { + // Drive Loomweave's REAL parser on the producer's full 25-field wire body. + let body = golden_body_string(EXAMPLE_NAME); + let parsed: IssueDetail = parse_issue_detail_response(&body) + .expect("Loomweave must accept Filigree's canonical issue-detail 200 body"); + + // The four modeled fields land off the REAL parser's output, asserted against + // the producer's source-of-truth literals. + assert_eq!(parsed.title, "golden issue detail"); + assert_eq!(parsed.status, "open"); + assert_eq!(parsed.priority, 2); +} + +#[test] +fn real_parser_routes_issue_id_into_id_via_the_weft_rename() { + // The contract's whole reason to exist: the weft vocabulary renames the + // entity's OWN primary key `id` → `issue_id`. The consumer's `IssueDetail.id` + // carries `#[serde(alias = "issue_id", default)]`, so it deserializes FROM the + // route's `issue_id`. Prove the alias is wired off the REAL parser's output. + let body_value = golden_response_body(EXAMPLE_NAME); + + // Sanity: the golden body carries `issue_id` and NOT the classic `id` (else + // this would assert the alias vacuously). + assert!( + body_value.get("issue_id").is_some(), + "golden body must carry the weft-renamed issue_id key" + ); + assert!( + body_value.get("id").is_none(), + "golden body must NOT carry the classic id key (it is renamed to issue_id)" + ); + + let parsed: IssueDetail = parse_issue_detail_response(&body_value.to_string()) + .expect("golden 200 body must parse"); + assert_eq!( + parsed.id, "genp-0123456789", + "id deserializes from the route's issue_id field (weft-4a46553503)" + ); +} + +#[test] +fn real_parser_ignores_the_unmodelled_producer_fields() { + // `IssueDetail` models only 4 of the 25 IssueWeft fields. The rest + // (status_category, type, parent_id, assignee, the lifecycle/commit anchors, + // description, notes, fields, labels, blocks, blocked_by, is_ready, children, + // data_warnings, the four *_at timestamps) are present in the golden body but + // NOT modelled. The consumer must IGNORE them additively (no + // `deny_unknown_fields`) so it keeps parsing as Filigree grows the route. + let body_value = golden_response_body(EXAMPLE_NAME); + + // Sanity: the golden body really does carry those unmodelled keys (otherwise + // this test would pass vacuously). + for unmodelled in [ + "status_category", + "type", + "parent_id", + "assignee", + "claimed_at", + "created_at", + "is_ready", + "children", + "data_warnings", + ] { + assert!( + body_value.get(unmodelled).is_some(), + "golden body must carry the unmodelled producer field {unmodelled} \ + (else the ignore-unknown assertion is vacuous)" + ); + } + + // The REAL parser accepts the body despite the 21 extra fields. + let parsed: IssueDetail = parse_issue_detail_response(&body_value.to_string()) + .expect("consumer must tolerate the unmodelled additive producer fields"); + assert_eq!(parsed.id, "genp-0123456789"); +} + +// ── NON-CIRCULAR negative cases — the discriminator for THIS seam ───────────── +// +// `title`/`status`/`priority` carry NO `#[serde(default)]`, and `priority` is +// typed `i64`. A positive parse cannot prove that (a defaulted or string-typed +// field would parse the full golden identically). These negatives drive the REAL +// parser on a MUTATED golden body and assert it hard-fails — making "dropping or +// retyping any required field hard-fails" executable, not aspirational. + +/// Take the golden 200 body, apply `mutate`, re-serialize, feed to the REAL +/// parser, and assert it returns `Err`. The base body parses (asserted above), so +/// any `Err` here is attributable to the mutation alone. +fn assert_mutation_rejected(mutate: impl FnOnce(&mut serde_json::Map), why: &str) { + let mut body_value = golden_response_body(EXAMPLE_NAME); + let obj = body_value + .as_object_mut() + .expect("golden 200 body is a JSON object"); + mutate(obj); + let result = parse_issue_detail_response(&Value::Object(obj.clone()).to_string()); + assert!(result.is_err(), "REAL parser must reject {why}: {result:?}"); +} + +#[test] +fn real_parser_rejects_a_dropped_title() { + // `title` is required (no serde default): a producer that stopped emitting it + // is a breaking shape regression, not a tolerable degrade. + assert_mutation_rejected( + |obj| { + obj.remove("title"); + }, + "a body missing the required `title` field", + ); +} + +#[test] +fn real_parser_rejects_a_dropped_status() { + // `status` is required (no serde default). + assert_mutation_rejected( + |obj| { + obj.remove("status"); + }, + "a body missing the required `status` field", + ); +} + +#[test] +fn real_parser_rejects_a_dropped_priority() { + // `priority` is required (no serde default). + assert_mutation_rejected( + |obj| { + obj.remove("priority"); + }, + "a body missing the required `priority` field", + ); +} + +#[test] +fn real_parser_rejects_a_string_typed_priority() { + // `priority` is typed `i64`. A producer that emitted it as a JSON string + // (`"2"`) is a retyping regression the consumer must reject, not coerce. + assert_mutation_rejected( + |obj| { + obj.insert("priority".to_owned(), Value::String("2".to_owned())); + }, + "a `priority` retyped to a JSON string", + ); +} + +#[test] +fn real_parser_rejects_a_null_priority() { + // `priority` is a non-optional `i64`; a null is a retyping/drop regression. + assert_mutation_rejected( + |obj| { + obj.insert("priority".to_owned(), Value::Null); + }, + "a `priority` retyped to JSON null", + ); +} + +// ── Layer 2 — drift recheck vs the Filigree producer source of truth ───────── + +/// Env var that ARMS the cross-repo drift recheck: when set truthy +/// (`1`/`true`/`yes`/`on`), an absent sibling Filigree repo becomes a hard +/// FAILURE instead of a skip-clean, so a release-gate run can require the +/// cross-repo authority recheck to actually execute rather than silently skip. +/// +/// NOT YET WIRED INTO CI: no job in `.github/workflows/{ci,release,verify}.yml` +/// currently sets this env or checks out the sibling Filigree repo (`FILIGREE_REPO`), +/// so in automation `vendored_golden_matches_filigree_authority` skips clean — +/// the byte-drift binding is developer-local / release-gate-manual, not +/// CI-enforced. The mechanism below is verified (the 2x2 `drift_check_action` +/// unit tests); only the trigger is absent. Wiring a release-gate job that sets +/// `LOOMWEAVE_DRIFT_REQUIRED=1` with `FILIGREE_REPO` pointed at a sibling checkout +/// is the remaining step to make this gate non-decorative in CI. +const DRIFT_REQUIRED_ENV: &str = "LOOMWEAVE_DRIFT_REQUIRED"; + +/// The action the Layer-2 recheck must take, given (a) whether the recheck is +/// armed as REQUIRED and (b) whether the sibling Filigree authority fixture is +/// present. Pure + total over the 2×2 so all three outcomes are unit-testable +/// without touching process-global env (which `cargo`'s parallel test threads +/// share) or the filesystem. +#[derive(Debug, PartialEq, Eq)] +enum DriftCheck { + /// Sibling present — byte-compare the vendored golden against the authority. + Compare, + /// Sibling absent and the recheck is NOT armed — skip cleanly (CI / detached + /// checkout). Layer-1 byte-pin + the vendored copy still gate the run. + SkipClean, + /// Sibling absent but the recheck IS armed (`LOOMWEAVE_DRIFT_REQUIRED=1`) — + /// fail: a release-gate run must not skip the cross-repo authority check. + FailRequired, +} + +fn drift_check_action(required: bool, authority_exists: bool) -> DriftCheck { + match (authority_exists, required) { + (true, _) => DriftCheck::Compare, + (false, false) => DriftCheck::SkipClean, + (false, true) => DriftCheck::FailRequired, + } +} + +fn drift_required() -> bool { + matches!( + std::env::var(DRIFT_REQUIRED_ENV) + .unwrap_or_default() + .trim() + .to_ascii_lowercase() + .as_str(), + "1" | "true" | "yes" | "on" + ) +} + +#[test] +fn vendored_golden_matches_filigree_authority() { + let repo = std::env::var("FILIGREE_REPO").unwrap_or_else(|_| "/home/john/filigree".to_owned()); + let authority: PathBuf = PathBuf::from(repo) + .join("tests") + .join("fixtures") + .join("contracts") + .join("weft") + .join("issues-get.json"); + + match drift_check_action(drift_required(), authority.exists()) { + DriftCheck::SkipClean => { + eprintln!( + "filigree authority fixture not found at {} — skipping Layer-2 drift recheck \ + (set FILIGREE_REPO to enable, or {DRIFT_REQUIRED_ENV}=1 to make absence a failure)", + authority.display() + ); + } + DriftCheck::FailRequired => { + panic!( + "filigree authority fixture not found at {} but {DRIFT_REQUIRED_ENV} is set — \ + the cross-repo drift recheck is REQUIRED; make the sibling Filigree repo \ + available (FILIGREE_REPO) or unset {DRIFT_REQUIRED_ENV}", + authority.display() + ); + } + DriftCheck::Compare => { + let authority_bytes = + std::fs::read(&authority).expect("read filigree authority fixture"); + assert_eq!( + authority_bytes, + GOLDEN.as_bytes(), + "vendored golden has DRIFTED from the Filigree authority at {} (filigree is the \ + PRODUCER — its copy is the authority); re-vendor BYTE-IDENTICAL", + authority.display() + ); + } + } +} + +#[test] +fn drift_check_action_compares_when_authority_present() { + assert_eq!(drift_check_action(false, true), DriftCheck::Compare); + assert_eq!(drift_check_action(true, true), DriftCheck::Compare); +} + +#[test] +fn drift_check_action_skips_clean_when_absent_and_unarmed() { + assert_eq!(drift_check_action(false, false), DriftCheck::SkipClean); +} + +#[test] +fn drift_check_action_fails_when_absent_but_required() { + assert_eq!(drift_check_action(true, false), DriftCheck::FailRequired); +} diff --git a/docs/federation/fixtures/filigree-issues-get.json b/docs/federation/fixtures/filigree-issues-get.json new file mode 100644 index 0000000..c9efff0 --- /dev/null +++ b/docs/federation/fixtures/filigree-issues-get.json @@ -0,0 +1,85 @@ +{ + "_meta": { + "contract": "filigree-weft", + "stability": "Frozen for the weft-generation lifetime per ADR-002. Shape additions that preserve wire compatibility (new optional response fields) are allowed; breaking changes require a new generation.", + "authority": [ + "docs/architecture/decisions/ADR-002-api-generations-and-federation-posture.md", + "docs/plans/2026-04-24-2.0-federation-work-package.md" + ], + "pinning_discipline": "shape reference; see docs/federation/contracts.md", + "updated": "2026-04-26", + "endpoint": "GET /api/weft/issues/{issue_id}", + "status": "live as of Phase C3.", + "relation_to_classic": "Classic GET /api/issue/{id} returns EnrichedIssueDetail (IssueDict + dep_details + events + comments). Weft returns the simpler IssueWeft (or IssueWeftWithFiles when include_files=true). Federation consumers do not need dashboard-UI enrichment; weft mirrors MCP get_issue's projection." + }, + "shape_decl": { + "request_type": "GET (no body); query param include_files (default false in weft)", + "response_type": "IssueWeft | IssueWeftWithFiles", + "rationale": "issue_id replaces id at the entity primary-key level; reference fields (parent_id, blocks, blocked_by, children) keep their existing names. include_files defaults to false for weft (federation consumers usually want the bare projection); classic does not expose include_files at all and continues returning EnrichedIssueDetail." + }, + "examples": [ + { + "name": "live_v_issue_detail_200", + "note": "Open, un-claimed task issue → 200 with the full 25-field IssueWeft projection. The entity's own primary key is named issue_id (the id→issue_id weft rename); reference fields keep their names. status_category derives from status; is_ready is computed from blockers; the four lifecycle/commit anchors are null for an open, un-claimed, un-closed issue.", + "request": { + "method": "GET", + "path": "/api/weft/issues/genp-0123456789", + "headers": {}, + "body": null + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "issue_id": "genp-0123456789", + "title": "golden issue detail", + "status": "open", + "status_category": "open", + "priority": 2, + "type": "task", + "parent_id": null, + "assignee": "", + "claimed_at": null, + "last_heartbeat_at": null, + "claim_expires_at": null, + "created_at": "2026-01-02T03:04:05+00:00", + "updated_at": "2026-01-02T03:04:05+00:00", + "closed_at": null, + "claim_commit": null, + "close_commit": null, + "description": "", + "notes": "", + "fields": {}, + "labels": [], + "blocks": [], + "blocked_by": [], + "is_ready": true, + "children": [], + "data_warnings": [] + } + } + }, + { + "name": "error_not_found", + "note": "Missing issue id \u2192 404 NOT_FOUND with the standard ErrorResponse envelope.", + "request": { + "method": "GET", + "path": "/api/weft/issues/genp-deadbeef00", + "headers": {}, + "body": null + }, + "response": { + "status": 404, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error": "Issue not found: genp-deadbeef00", + "code": "NOT_FOUND" + } + } + } + ] +} diff --git a/docs/federation/fixtures/loomweave-scan-results-wire.golden.json b/docs/federation/fixtures/loomweave-scan-results-wire.golden.json new file mode 100644 index 0000000..3f20f1e --- /dev/null +++ b/docs/federation/fixtures/loomweave-scan-results-wire.golden.json @@ -0,0 +1,73 @@ +{ + "scan_source": "loomweave", + "scan_run_id": "run-conformance-1", + "mark_unseen": true, + "create_observations": false, + "complete_scan_run": true, + "findings": [ + { + "line_end": 20, + "line_start": 12, + "message": "Circular import detected", + "metadata": { + "confidence": 0.95, + "confidence_basis": "ast_match", + "kind": "defect", + "loomweave": { + "entity_id": "python:class:auth.tokens::TokenManager", + "internal_severity": "WARN", + "internal_status": "open", + "related_entities": [ + "python:class:auth.sessions::SessionStore" + ], + "supported_by": [], + "supports": [ + "python:function:auth.tokens::mint" + ] + } + }, + "path": "src/auth/tokens.py", + "rule_id": "LMWV-PY-STRUCTURE-001", + "severity": "medium" + }, + { + "line_end": 58, + "line_start": 42, + "message": "HTTP route entry point", + "metadata": { + "kind": "fact", + "loomweave": { + "entity_id": "python:function:api.routes::handle_request", + "internal_severity": "NONE", + "internal_status": "open", + "related_entities": [], + "supported_by": [], + "supports": [] + } + }, + "path": "src/api/routes.py", + "rule_id": "LMWV-PY-FACT-ENTRYPOINT", + "severity": "info" + }, + { + "message": "Subsystem exhibits weak modularity", + "metadata": { + "confidence": 0.5, + "confidence_basis": "coupling_metric", + "kind": "defect", + "loomweave": { + "entity_id": "core:subsystem:abcd1234", + "internal_severity": "ERROR", + "internal_status": "open", + "related_entities": [], + "supported_by": [], + "supports": [], + "synthetic_anchor": true + } + }, + "path": "/repo/root", + "rule_id": "LMWV-SUBSYSTEM-COHESION", + "severity": "high" + } + ] +} From 2a0bb91c57be369bb74674466cff66c5f38573d8 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:37:52 +1000 Subject: [PATCH 5/6] fix(plugin-rust): root edition-2024 #[unsafe(no_mangle)]/#[unsafe(export_name)] FFI entry-points (ADR-054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `attr_is_ident_in` matched only the bare `#[no_mangle]` / `#[export_name]` forms. Edition 2024 (this workspace's pinned edition) makes those a hard error; real FFI code writes `#[unsafe(no_mangle)]`, which syn parses as `Meta::List { path: "unsafe", tokens: }`. The export ident was missed, so every edition-2024 FFI export went un-rooted and read as dead — the under-rooting failure ADR-054's error-cost asymmetry exists to prevent. Peel a single `#[unsafe(...)]` wrapper (inner parsed as a full `Meta` to cover both the bare-path `no_mangle` and the name-value `export_name = "…"` forms) before the ident check. Add tests for both wrapped forms — the existing FFI tests used the bare form and masked the gap. Also correct a stale doc comment in shortcuts.rs that claimed the Rust plugin emits no root tags (false since ADR-054 shipped them). Found in detailed pre-push review. Follow-ups filed: clarion-15d11462a6 (module-scope #[allow(dead_code)] / cfg(test) under-rooting), clarion-736ad98353 (root-tag test hardening), clarion-86200a39d2 (http-route faceted precision). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../loomweave-mcp/src/catalogue/shortcuts.rs | 10 +++--- crates/loomweave-plugin-rust/src/root_tags.rs | 30 +++++++++++++--- .../loomweave-plugin-rust/tests/root_tags.rs | 34 +++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/crates/loomweave-mcp/src/catalogue/shortcuts.rs b/crates/loomweave-mcp/src/catalogue/shortcuts.rs index c7a939f..39a93af 100644 --- a/crates/loomweave-mcp/src/catalogue/shortcuts.rs +++ b/crates/loomweave-mcp/src/catalogue/shortcuts.rs @@ -1072,11 +1072,11 @@ struct CandidateSet { /// /// - non-code KINDS (file anchors, the project anchor, subsystems, guidance) /// are never dead-CODE candidates; -/// - a plugin that emitted NO reachability root tags (the Rust plugin today — -/// binary/lib roots unsupported) would have its ENTIRE entity set -/// false-flagged dead, so its entities are excluded and counted for the -/// in-band scope marker instead — a wrong answer is worse than an honest -/// scope statement (the dogfooded `specimen-rs/src/main.rs` false positive). +/// - a plugin that emitted NO reachability root tags would have its ENTIRE +/// entity set false-flagged dead, so its entities are excluded and counted +/// for the in-band scope marker instead — a wrong answer is worse than an +/// honest scope statement (the Rust plugin hit this before ADR-054 gave it +/// root tags; cf. the dogfooded `specimen-rs/src/main.rs` false positive). fn dead_code_candidate_set( conn: &rusqlite::Connection, reachable: &HashSet, diff --git a/crates/loomweave-plugin-rust/src/root_tags.rs b/crates/loomweave-plugin-rust/src/root_tags.rs index 7a5936b..0f47824 100644 --- a/crates/loomweave-plugin-rust/src/root_tags.rs +++ b/crates/loomweave-plugin-rust/src/root_tags.rs @@ -192,12 +192,32 @@ fn attr_last_seg_in(attrs: &[Attribute], names: &[&str]) -> bool { }) } -/// Any attribute that is the bare single ident `name` (no path) for one of -/// `names` — used where the attribute is never path-qualified (`#[proc_macro]`). +/// Any attribute whose path ident — after peeling a single edition-2024 +/// `#[unsafe()]` wrapper — is one of `names`. Covers both a bare +/// single-ident attribute (`#[proc_macro]`, never path-qualified) and the +/// unsafe-wrapped FFI exports: edition 2024 makes bare `#[no_mangle]` / +/// `#[export_name = "…"]` a hard error, so real code writes +/// `#[unsafe(no_mangle)]` / `#[unsafe(export_name = "…")]`, which syn parses as +/// `Meta::List { path: "unsafe", tokens: }` — the export ident +/// lives one level in. The inner may be a bare path (`no_mangle`) or a +/// name-value (`export_name = "…"`), so it is parsed as a full [`Meta`]. Missing +/// the wrapped form would under-root every edition-2024 FFI export (read dead). fn attr_is_ident_in(attrs: &[Attribute], names: &[&str]) -> bool { - attrs - .iter() - .any(|a| names.iter().any(|n| a.path().is_ident(n))) + attrs.iter().any(|a| { + if names.iter().any(|n| a.path().is_ident(n)) { + return true; + } + let Meta::List(list) = &a.meta else { + return false; + }; + if !list.path.is_ident("unsafe") { + return false; + } + let Ok(inner) = syn::parse2::(list.tokens.clone()) else { + return false; + }; + names.iter().any(|n| inner.path().is_ident(n)) + }) } /// Any `#[derive(...)]` whose derive list contains a path with a final segment diff --git a/crates/loomweave-plugin-rust/tests/root_tags.rs b/crates/loomweave-plugin-rust/tests/root_tags.rs index fb7bdd2..07ee74d 100644 --- a/crates/loomweave-plugin-rust/tests/root_tags.rs +++ b/crates/loomweave-plugin-rust/tests/root_tags.rs @@ -116,6 +116,24 @@ fn no_mangle_ffi_export_is_entry_point_and_exported_api() { ); } +#[test] +fn unsafe_no_mangle_ffi_export_is_entry_point_and_exported_api() { + // Edition 2024 makes bare `#[no_mangle]` a hard error; real FFI code writes + // `#[unsafe(no_mangle)]`, which syn parses as `Meta::List { path: "unsafe", + // tokens: "no_mangle" }`. The export ident lives one level in — it must + // still root, or every edition-2024 FFI export reads as dead (the + // under-rooting failure ADR-054 fights). + let m = tags_by_id( + "k", + "k.m", + "#[unsafe(no_mangle)]\npub extern \"C\" fn ffi() {}\n", + ); + assert_eq!( + tags(&m, "rust:function:k.m.ffi"), + ["entry-point", "exported-api"] + ); +} + // ---- test ----------------------------------------------------------------- #[test] @@ -223,6 +241,22 @@ fn export_name_ffi_export_is_entry_point() { ); } +#[test] +fn unsafe_export_name_ffi_export_is_entry_point() { + // Edition-2024 wrapped form (see `unsafe_no_mangle_…`): `#[unsafe(export_name + // = "…")]` parses as `unsafe()`, so the inner `export_name` ident + // must be reached through the wrapper. + let m = tags_by_id( + "k", + "k.m", + "#[unsafe(export_name = \"my_export\")]\npub extern \"C\" fn exported() {}\n", + ); + assert_eq!( + tags(&m, "rust:function:k.m.exported"), + ["entry-point", "exported-api"] + ); +} + // ---- regression guard: serde/typetag must NOT be mistaken for a root ------- #[test] From e5b6e4b6ab8166f2ff6780be15bced20ccdfe92b Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:38:32 +1000 Subject: [PATCH 6/6] style: satisfy fmt/clippy/ruff on the conformance test files The feat/weft-taint-conformance test files were committed without final fmt/lint passes and tripped three floor gates: - cargo fmt line-wrapping in three Rust conformance oracles; - clippy::doc_markdown backtick on `shape_decl` in serve.rs; - ruff format on the python wardline vocabulary-descriptor conformance test. Whitespace/doc-comment only; no behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/loomweave-cli/tests/serve.rs | 2 +- .../tests/scan_results_wire_conformance_oracle.rs | 14 +++++++++++--- .../entity_associations_conformance_oracle.rs | 6 +++--- .../tests/issue_detail_conformance_oracle.rs | 4 ++-- ...t_wardline_vocabulary_descriptor_conformance.py | 4 +++- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/loomweave-cli/tests/serve.rs b/crates/loomweave-cli/tests/serve.rs index 24f41aa..9d6712d 100644 --- a/crates/loomweave-cli/tests/serve.rs +++ b/crates/loomweave-cli/tests/serve.rs @@ -825,7 +825,7 @@ fn serve_http_files_storage_failure_returns_closed_error_without_raw_detail() { /// The `GET /api/v1/_capabilities` federation golden, embedded BYTE-IDENTICAL /// from the on-disk normative fixture. `include_str!` captures the exact bytes -/// (meta + shape_decl + examples), so this const IS the wire authority the +/// (meta + `shape_decl` + examples), so this const IS the wire authority the /// producer-recheck tests (`serve_http_responses_match_federation_fixture_contracts` /// and `serve_http_capabilities_and_mcp_stdio_coexist`) compare the live /// `get_capabilities` handler against. diff --git a/crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs b/crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs index 8fac9e3..ea78d95 100644 --- a/crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs +++ b/crates/loomweave-federation/tests/scan_results_wire_conformance_oracle.rs @@ -161,7 +161,8 @@ fn produced_batch() -> PreparedBatch { /// HTTP client serializes it (`reqwest .json()` → `serde_json::to_*` over the same /// `Serialize` impl). fn produced_wire_value() -> Value { - serde_json::to_value(&produced_batch().request).expect("serialize produced scan-results request") + serde_json::to_value(&produced_batch().request) + .expect("serialize produced scan-results request") } fn golden_value() -> Value { @@ -232,7 +233,11 @@ fn producer_emits_the_loomweave_positive_contract() { assert_eq!(wire["complete_scan_run"], Value::Bool(true)); let findings = wire["findings"].as_array().expect("findings array"); - assert_eq!(findings.len(), 3, "all three fixed rows emit (none skipped)"); + assert_eq!( + findings.len(), + 3, + "all three fixed rows emit (none skipped)" + ); // Row 0 — defect: WARN→medium, real path + lines, nested loomweave.* round-trip. let defect = &findings[0]; @@ -243,7 +248,10 @@ fn producer_emits_the_loomweave_positive_contract() { assert_eq!(defect["line_end"], Value::from(20)); assert_eq!(defect["metadata"]["kind"], Value::from("defect")); assert_eq!(defect["metadata"]["confidence"], Value::from(0.95)); - assert_eq!(defect["metadata"]["confidence_basis"], Value::from("ast_match")); + assert_eq!( + defect["metadata"]["confidence_basis"], + Value::from("ast_match") + ); let lw = &defect["metadata"]["loomweave"]; assert_eq!( lw["entity_id"], diff --git a/crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs b/crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs index c37c19d..8ae1feb 100644 --- a/crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs +++ b/crates/loomweave-mcp/tests/entity_associations_conformance_oracle.rs @@ -118,9 +118,9 @@ fn golden_response_body(example_name: &str) -> String { .get("examples") .and_then(Value::as_array) .and_then(|examples| { - examples.iter().find(|example| { - example.get("name").and_then(Value::as_str) == Some(example_name) - }) + examples + .iter() + .find(|example| example.get("name").and_then(Value::as_str) == Some(example_name)) }) .and_then(|example| example.pointer("/response/body")) .unwrap_or_else(|| panic!("missing fixture example body {example_name}")) diff --git a/crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs b/crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs index 3c38451..fe51ba6 100644 --- a/crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs +++ b/crates/loomweave-mcp/tests/issue_detail_conformance_oracle.rs @@ -187,8 +187,8 @@ fn real_parser_routes_issue_id_into_id_via_the_weft_rename() { "golden body must NOT carry the classic id key (it is renamed to issue_id)" ); - let parsed: IssueDetail = parse_issue_detail_response(&body_value.to_string()) - .expect("golden 200 body must parse"); + let parsed: IssueDetail = + parse_issue_detail_response(&body_value.to_string()).expect("golden 200 body must parse"); assert_eq!( parsed.id, "genp-0123456789", "id deserializes from the route's issue_id field (weft-4a46553503)" diff --git a/plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py b/plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py index 72ac5c4..54323a7 100644 --- a/plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py +++ b/plugins/python/tests/test_wardline_vocabulary_descriptor_conformance.py @@ -274,7 +274,9 @@ def test_consumer_version_gate_rejects_skew_copy(tmp_path: Path) -> None: # proving the gate keys on version, not on a parse failure. assert state.vocabulary is not None assert state.vocabulary.confidence_basis == "descriptor_version_skew" - assert tuple(sorted(state.vocabulary.entries_by_name)) == tuple(sorted(EXPECTED_CANONICAL_NAMES)) + assert tuple(sorted(state.vocabulary.entries_by_name)) == tuple( + sorted(EXPECTED_CANONICAL_NAMES) + ) # ── Layer 2 — drift recheck vs the Wardline source of truth ──────────────────