newt-core 0.8.0

Newt-Agent core types, errors, and the NeMoCode-style tier router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//! The one owner of the model-facing disposition vocabulary (#2051).
//!
//! # Ownership boundary
//!
//! This module owns the four channels through which the harness speaks to the
//! model *about* the turn's disposition, and nothing else:
//!
//! 1. the `[NEWT PROMPT COMPREHENSION]` card block ([`super::PromptIntake::model_card`]),
//! 2. the dispatcher's refusal when a call falls outside the disposition
//!    (`tools/catalog.rs`),
//! 3. the `tool_search` scope note on a filtered catalog (`tool_search.rs`),
//! 4. the next-turn scope sentence in the `select_operating_mode` tool
//!    description (`operating_mode.rs`).
//!
//! It does **not** own the base identity (`memory::DEFAULT_SOUL`), which names
//! every disposition and advertises tools by name on every turn. That is
//! identity text with its own owner and its own issue; the ratchet at the
//! bottom of this file checks every owned output against the four sites
//! above, no more and no less.
//!
//! Before this module the same vocabulary was hand-written at those four
//! sites, which is how the dispatcher came to greet a model with "This is an
//! Explain turn" while the card said something else — and how a 9b model came
//! to read one of those sentences back to the operator verbatim.
//!
//! # Three rules, each with a test
//!
//! - **Provenance is truthful.** The card says where the disposition came
//!   from, and there are two honest answers: the intake lexicon read it off
//!   the prompt, or a session setting the operator chose earlier narrowed it
//!   (`/mode plan`, `/mode diagnose`). Saying "the operator did not choose it"
//!   in the second case is false, and a small model repeats what it is told.
//! - **The mechanism is never named in a refusal.** No "disposition", "mode",
//!   or "turn" reaches the model from a denial, because the word is what it
//!   echoes. The card is structured plumbing and keeps its keys; the privacy
//!   clause tells the model not to read them aloud.
//! - **A refusal bounds what may be *quoted*, never what must be *said*.** The
//!   model may not recite this notice or an internal policy, and it must still
//!   say plainly what remains undone. The first version of this text said only
//!   "do not report this refusal", and a 9b model, denied its write and told
//!   to stay quiet about it, reported the write as done instead.

use super::prompt_intake::{DispositionSource, PromptDisposition};

/// The model-facing lines for one disposition.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DispositionVoice {
    /// Which disposition this entry speaks for.
    pub(crate) disposition: PromptDisposition,
    /// The `harness_action:` line in the active-prompt comprehension card.
    pub(crate) card_action: String,
    /// What is available instead, when a call is refused under this
    /// disposition. Empty for [`PromptDisposition::Act`], which refuses nothing.
    pub(crate) denied_guidance: String,
}

/// The complete disposition vocabulary: one [`DispositionVoice`] per
/// disposition plus the clauses shared by all of them.
///
/// Crate-private on purpose: no production caller threads a configured
/// instance through the consumers yet, and a public override surface with no
/// consumer is a half-live knob. When an `[intake]` override lands, it lands
/// here as a value, the way [`super::DispositionLexicon`] already does.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DispositionVoices {
    voices: Vec<DispositionVoice>,
    /// Card provenance when the lexicon classified the prompt.
    pub(crate) provenance_inferred: String,
    /// Card provenance when a session setting narrowed the classification.
    pub(crate) provenance_policy: String,
    /// Marks the card as plumbing and bounds what may be said about it.
    pub(crate) privacy: String,
    /// Appended to every refusal: what may not be quoted, and what must
    /// still be said.
    pub(crate) denied_privacy: String,
    /// How `tool_search` lists a tool the session grants but this request does
    /// not admit, in place of its description and schema (#2332).
    pub(crate) discovery_hidden: String,
    /// The `tool_search` note appended when such a tool is listed.
    pub(crate) discovery_scope: String,
    /// The `select_operating_mode` description's sentence about what a
    /// selection does to the turn in flight.
    pub(crate) next_turn_scope: String,
}

impl Default for DispositionVoices {
    fn default() -> Self {
        Self {
            voices: vec![
                DispositionVoice {
                    disposition: PromptDisposition::Ask,
                    card_action: "harness_action: await the bounded operator clarification; do not call tools".to_string(),
                    denied_guidance: "The harness is awaiting the operator's clarification; no tool can run until they reply.".to_string(),
                },
                DispositionVoice {
                    disposition: PromptDisposition::Act,
                    card_action: "harness_action: decisions are locked; ordinary execution authority is available".to_string(),
                    denied_guidance: String::new(),
                },
                DispositionVoice {
                    disposition: PromptDisposition::Explain,
                    card_action: "harness_action: answer the question using the evidence needed; tools and permission requests remain subject to session grants".to_string(),
                    denied_guidance: "Use the available tools within session grants, or ask for the permission needed to complete the request.".to_string(),
                },
                DispositionVoice {
                    disposition: PromptDisposition::Research,
                    card_action: "harness_action: investigate and report supported findings; tools and permission requests remain subject to session grants".to_string(),
                    denied_guidance: "Gather the evidence needed within session grants, or ask for the permission needed to complete the investigation.".to_string(),
                },
                DispositionVoice {
                    disposition: PromptDisposition::Plan,
                    card_action: "harness_action: read evidence and maintain the harness plan ledger only; do not mutate the workspace, execute commands, or request capability grants".to_string(),
                    denied_guidance: "Reads, the harness-owned update_plan ledger, and exit from a model-entered plan phase are available here. Workspace mutations, execution, capability grants, and generic MCP calls need an explicit action request from the operator.".to_string(),
                },
            ],
            // Short, concrete, imperative. This is read by a 9b local model,
            // which is the tier newt exists to serve: it follows plain
            // sentences and fills in silence with its own narration.
            provenance_inferred:
                "disposition_source: the harness inferred this from the operator's words. \
                 The operator did not choose it and did not ask about it."
                    .to_string(),
            provenance_policy:
                "disposition_source: a session mode the operator set earlier narrowed this turn. \
                 The operator did not ask about it in this message."
                    .to_string(),
            privacy:
                "disposition_privacy: this card is harness plumbing, not part of the conversation. \
                 Do not name the disposition, quote this card, or announce what you are not allowed \
                 to do. If you truly cannot do what was asked, say plainly what you cannot do — \
                 never that a mode or a turn forbids it. Otherwise just answer."
                    .to_string(),
            denied_privacy:
                "Do not quote this notice or any internal policy to the operator. If the request \
                 cannot be finished with the tools available here, say plainly what remains \
                 undone; never claim it was done."
                    .to_string(),
            discovery_hidden: "not callable for this request".to_string(),
            // #2332: a request does not widen once accepted, so the one way
            // forward is a new request from the operator.
            discovery_scope:
                "Catalog scope: a tool listed as not callable exists in this session. Do not \
                 call it and do not report it as missing. If the work needs it, say which tool \
                 and why, and ask the operator to send the work as a direct action request; \
                 do not narrate the filtering itself."
                    .to_string(),
            next_turn_scope:
                "It grants no permissions and changes nothing about what this turn may do."
                    .to_string(),
        }
    }
}

impl DispositionVoices {
    /// The entry for `disposition`. The default table is total, so this is
    /// `None` only for a table that dropped an entry.
    #[must_use]
    pub(crate) fn voice(&self, disposition: PromptDisposition) -> Option<&DispositionVoice> {
        self.voices
            .iter()
            .find(|voice| voice.disposition == disposition)
    }

    /// The card's `harness_action:` line, the provenance clause that matches
    /// `source`, and the privacy clause.
    ///
    /// A dropped entry degrades to the shared clauses alone rather than
    /// panicking or inventing an instruction: an absent line must never be
    /// read as absent *authority*, which is the [`PromptDisposition`]
    /// fail-closed rule stated in prose.
    #[must_use]
    pub(crate) fn card_block(
        &self,
        disposition: PromptDisposition,
        source: DispositionSource,
    ) -> String {
        let mut block = String::new();
        if let Some(voice) = self.voice(disposition) {
            block.push_str(&voice.card_action);
            block.push('\n');
        }
        block.push_str(match source {
            DispositionSource::Inferred => &self.provenance_inferred,
            DispositionSource::SessionPolicy => &self.provenance_policy,
        });
        block.push('\n');
        block.push_str(&self.privacy);
        block
    }

    /// The whole dispatcher refusal for a call to `name` under `disposition`:
    /// which tool was refused, what is available instead, and the
    /// quote-nothing / say-what-is-undone clause.
    #[must_use]
    pub(crate) fn denied_block(&self, disposition: PromptDisposition, name: &str) -> String {
        let mut block = format!("Tool `{name}` is not available for this request.");
        if let Some(voice) = self.voice(disposition) {
            if !voice.denied_guidance.is_empty() {
                block.push(' ');
                block.push_str(&voice.denied_guidance);
            }
        }
        block.push(' ');
        block.push_str(&self.denied_privacy);
        block
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const EVERY_DISPOSITION: [PromptDisposition; 5] = [
        PromptDisposition::Ask,
        PromptDisposition::Act,
        PromptDisposition::Explain,
        PromptDisposition::Research,
        PromptDisposition::Plan,
    ];

    const BOTH_SOURCES: [DispositionSource; 2] = [
        DispositionSource::Inferred,
        DispositionSource::SessionPolicy,
    ];

    #[test]
    fn evidence_style_guidance_defers_authority_to_session_grants() {
        let voices = DispositionVoices::default();
        for disposition in [PromptDisposition::Explain, PromptDisposition::Research] {
            let voice = voices.voice(disposition).expect("configured voice");
            for text in [&voice.card_action, &voice.denied_guidance] {
                assert!(text.contains("session grants"), "{text}");
                assert!(text.contains("permission"), "{text}");
                for stale in [
                    "bounded read/recovery tools only",
                    "Only the bounded read-only",
                    "do not mutate or request capability grants",
                    "need an explicit action request",
                ] {
                    assert!(!text.contains(stale), "{text}");
                }
            }
        }
    }

    /// Whole-word membership, so `model-entered` does not count as `mode`.
    fn names_any(text: &str, words: &[&str]) -> Option<String> {
        text.split(|c: char| !c.is_ascii_alphanumeric())
            .map(str::to_ascii_lowercase)
            .find(|word| words.contains(&word.as_str()))
    }

    #[test]
    fn the_default_table_speaks_for_every_disposition() {
        let voices = DispositionVoices::default();
        for disposition in EVERY_DISPOSITION {
            assert!(
                voices.voice(disposition).is_some(),
                "no voice for {disposition:?}: a new disposition must be given one here, \
                 not left to a silent default at a call site"
            );
        }
    }

    /// #2051: the observed defect. The card said what the model must do and
    /// nothing about where the instruction came from, so a 9b model reported
    /// its compliance to the operator.
    #[test]
    fn every_card_block_states_provenance_and_privacy() {
        let voices = DispositionVoices::default();
        for disposition in EVERY_DISPOSITION {
            for source in BOTH_SOURCES {
                let block = voices.card_block(disposition, source);
                assert!(
                    block.contains("disposition_source:"),
                    "{disposition:?}/{source:?} card must say where this came from: {block}"
                );
                assert!(
                    block.contains("disposition_privacy:"),
                    "{disposition:?}/{source:?} card must say it is not for the operator: {block}"
                );
            }
        }
    }

    /// Review of #2057: `/mode plan` and `/mode diagnose` narrow the turn on
    /// the operator's own standing instruction, so "the operator did not
    /// choose it" is false there. Each source gets the sentence that is true
    /// for it, and the false one never appears under the other.
    #[test]
    fn provenance_matches_the_source_it_claims() {
        let voices = DispositionVoices::default();
        let inferred = voices.card_block(PromptDisposition::Explain, DispositionSource::Inferred);
        assert!(
            inferred.contains("inferred this from the operator's words"),
            "{inferred}"
        );
        assert!(inferred.contains("did not choose it"), "{inferred}");

        let policy =
            voices.card_block(PromptDisposition::Explain, DispositionSource::SessionPolicy);
        assert!(
            policy.contains("a session mode the operator set"),
            "{policy}"
        );
        assert!(
            !policy.contains("did not choose it"),
            "a policy-narrowed card must not deny the operator's own choice: {policy}"
        );
        assert!(
            !policy.contains("inferred this"),
            "a policy-narrowed card must not claim the words decided it: {policy}"
        );
    }

    /// The privacy clause suppresses the *mechanism*, never the substance. A
    /// model that cannot do the work still owes the operator a plain answer,
    /// so the clause must keep saying so.
    #[test]
    fn the_privacy_clause_still_permits_saying_what_cannot_be_done() {
        let voices = DispositionVoices::default();
        assert!(
            voices.privacy.contains("say plainly what you cannot do"),
            "privacy must not become a gag on honest limits: {}",
            voices.privacy
        );
    }

    /// A refusal names the tool it refused, forbids quoting the notice, and
    /// separately requires saying what remains undone. The first version said
    /// only "do not report this refusal", and the observed 9b model, denied
    /// its write and told to stay quiet, reported the write as done.
    #[test]
    fn every_refusal_separates_quoting_from_saying() {
        let voices = DispositionVoices::default();
        for disposition in EVERY_DISPOSITION {
            let denied = voices.denied_block(disposition, "write_file");
            assert!(
                denied.starts_with("Tool `write_file` is not available"),
                "{denied}"
            );
            assert!(
                denied.contains("Do not quote this notice"),
                "{disposition:?} refusal must bound quoting: {denied}"
            );
            assert!(
                denied.contains("say plainly what remains undone"),
                "{disposition:?} refusal must still require the honest answer: {denied}"
            );
            assert!(
                denied.contains("never claim it was done"),
                "{disposition:?} refusal must forbid the false-success answer: {denied}"
            );
        }
    }

    /// The word the model echoes is the word it was handed. No refusal names
    /// the mechanism — not by its name, not as a mode, not as a turn — and
    /// none opens with the "This is an X turn" framing the evidenced model
    /// read aloud.
    #[test]
    fn no_refusal_names_the_mechanism() {
        let voices = DispositionVoices::default();
        for disposition in EVERY_DISPOSITION {
            let denied = voices.denied_block(disposition, "run_command");
            assert_eq!(
                names_any(&denied, &["disposition", "mode", "turn"]),
                None,
                "{disposition:?} refusal names the mechanism: {denied}"
            );
            assert!(
                !denied.contains("This is an") && !denied.contains("This is a "),
                "{disposition:?} refusal reintroduces the \"This is an X turn\" phrasing: {denied}"
            );
        }
        assert_eq!(
            names_any(&voices.next_turn_scope, &["disposition", "mode"]),
            None,
            "the operating-mode scope sentence names the mechanism: {}",
            voices.next_turn_scope
        );
    }

    /// A dropped entry must fail toward *less* instruction, never toward an
    /// invented one, and must still carry the shared clauses.
    #[test]
    fn a_dropped_entry_degrades_to_the_shared_clauses() {
        let mut voices = DispositionVoices::default();
        voices
            .voices
            .retain(|voice| voice.disposition != PromptDisposition::Explain);
        let block = voices.card_block(PromptDisposition::Explain, DispositionSource::Inferred);
        assert!(!block.contains("harness_action:"), "{block}");
        assert!(block.contains("disposition_source:"), "{block}");
        assert_eq!(
            voices.denied_block(PromptDisposition::Explain, "edit_file"),
            format!(
                "Tool `edit_file` is not available for this request. {}",
                voices.denied_privacy
            )
        );
    }

    /// Every site [`no_other_module_hand_writes_the_disposition_vocabulary`]
    /// scans, **validated on the way out** (#2191).
    ///
    /// That guard is a NEGATIVE assertion — `!source.contains(sentence)` — and
    /// a negative assertion fails OPEN against haystack loss: a source that
    /// contributes nothing satisfies it trivially. The owned sentences were
    /// already floored (`assert!(!text.is_empty())`); the haystacks were not.
    /// Measured rather than theorised — the control is in the #2191 thread.
    ///
    /// Modelled on `newt-tui`'s `production_source`: the check lives at the one
    /// place every consumer goes through, so a future consumer cannot forget to
    /// write it. It deliberately does NOT borrow that helper's literal-marker
    /// cut — that is the ceiling #2202 is about, and this guard scans whole
    /// files rather than production halves, so it has no reason to inherit it.
    fn scanned_sites() -> [(&'static str, &'static str); 12] {
        // `prompt_intake.rs` was split into `prompt_intake_tests/` (#2189 lane).
        // EVERY child is listed: this is a NEGATIVE assertion over the included
        // text, so a file that stops being scanned stops being checked without
        // ever going red — the vacuous-negative shape of #2150.
        const SITES: [(&str, &str); 12] = [
            ("prompt_intake.rs", include_str!("prompt_intake.rs")),
            (
                "prompt_intake_tests/atomic_ask.rs",
                include_str!("prompt_intake_tests/atomic_ask.rs"),
            ),
            (
                "prompt_intake_tests/card_and_artifact.rs",
                include_str!("prompt_intake_tests/card_and_artifact.rs"),
            ),
            (
                "prompt_intake_tests/clarification_gate.rs",
                include_str!("prompt_intake_tests/clarification_gate.rs"),
            ),
            (
                "prompt_intake_tests/disposition_inference.rs",
                include_str!("prompt_intake_tests/disposition_inference.rs"),
            ),
            (
                "prompt_intake_tests/imperative_recognition.rs",
                include_str!("prompt_intake_tests/imperative_recognition.rs"),
            ),
            (
                "prompt_intake_tests/informational_authority.rs",
                include_str!("prompt_intake_tests/informational_authority.rs"),
            ),
            (
                "prompt_intake_tests/mod.rs",
                include_str!("prompt_intake_tests/mod.rs"),
            ),
            (
                "prompt_intake_tests/negation.rs",
                include_str!("prompt_intake_tests/negation.rs"),
            ),
            ("tool_search.rs", include_str!("tool_search.rs")),
            ("tools/catalog.rs", include_str!("tools/catalog.rs")),
            ("operating_mode.rs", include_str!("operating_mode.rs")),
        ];

        // FLOOR 1 — PER ENTRY, never aggregate. An aggregate byte count would
        // let a 73 kB `tools/catalog.rs` mask an emptied 2 kB family file. The
        // per-file rows in `content_addressable_ratchet.rs` exist for exactly
        // this reason: "the aggregate alone would net to zero and report
        // success while the rule was being broken".
        //
        // The floor is "any text at all", NOT a byte count, and that is a
        // deliberate departure from `progress_sink.rs`'s `> 1_000`. Its three
        // subjects are whole driver modules; these twelve are not comparable.
        // The smallest legitimate member here is 2,294 bytes today, but a
        // one-test family file is half that and the decomposition lane keeps
        // producing smaller ones — a floor that tracks how big these files
        // happen to be must be revised every time one is split, and a floor
        // revised under time pressure is the one that gets lowered. This floor
        // is chosen for what makes `!contains` vacuous instead.
        for (name, source) in SITES {
            assert!(
                !source.trim().is_empty(),
                "{name} scanned as empty — a negative assertion over it would \
                 pass having read nothing"
            );
        }

        // FLOOR 2 — on the SET, which is the failure this guard actually had.
        //
        // #2190: `prompt_intake.rs` became eight children while this list still
        // named four sites, and the privacy clause pasted VERBATIM into a
        // relocated child passed. No per-entry check can see a site that is not
        // in the list, so completeness is DERIVED rather than pinned by hand:
        // every `#[path = "…"]` module a scanned source declares must itself be
        // scanned. Split a file into more children and forget this list, and
        // the guard fails instead of quietly narrowing.
        //
        // Applied to EVERY scanned source, not only module roots. A source
        // declaring no out-of-line module contributes nothing here, which is
        // correct — `tool_search.rs`, `tools/catalog.rs` and `operating_mode.rs`
        // declare none today. Restricting the check to `*/mod.rs` would have
        // missed `prompt_intake.rs`'s own declaration of the module root, and
        // that row going missing is the same defect one level up.
        let mut declarations = 0usize;
        for (name, source) in SITES {
            // `#[path]` on an out-of-line module resolves against the directory
            // holding the declaring file, which is what these names are
            // relative to.
            let dir = name.rsplit_once('/').map_or("", |(parent, _)| parent);
            for rest in source.split("#[path = \"").skip(1) {
                let declared = rest
                    .split('"')
                    .next()
                    .expect("a `#[path = \"…\"]` attribute closes its quote");
                declarations += 1;
                let expected = if dir.is_empty() {
                    declared.to_string()
                } else {
                    format!("{dir}/{declared}")
                };
                assert!(
                    SITES.iter().any(|(listed, _)| *listed == expected),
                    "{name} declares module `{declared}`, but {expected} is not \
                     scanned. A site that is not listed is a site this guard \
                     does not check — ADD the row; never drop one to make this \
                     pass."
                );
            }
        }

        // The parse is itself a predicate over a set, so it gets the same
        // treatment this whole guard is about. If rustfmt ever respelled the
        // attribute (`#[path="x.rs"]`, no spaces), every split above would
        // yield nothing and the loop would pass having found nothing — this
        // issue's own defect, reappearing inside the fix for it. Eight
        // declarations are parsed today; the floor is that SOMETHING is.
        assert!(
            declarations > 0,
            "no `#[path = \"…\"]` declaration parsed out of any scanned source, \
             so the completeness check above read nothing. The attribute's \
             exact spelling is load-bearing here."
        );

        // What this does NOT cover, stated so it is not read as more than it
        // is: dropping a row for a file that is not a module root — say
        // `tool_search.rs` — is still caught only in review. Nothing available
        // at compile time enumerates "every file that could hand-write this
        // vocabulary". Floor 2 covers the SPLIT case, which is the one that
        // actually failed.
        SITES
    }

    /// The single-owner ratchet, over every owned output and every consuming
    /// site. `include_str!` is compile-time, so this stays inside the
    /// fully-mocked unit tier — no filesystem at run time.
    ///
    /// Four sites once carried their own copy of this vocabulary. The count
    /// may only go DOWN, and a site that reconstructs any owned sentence by
    /// hand — a card line, a refusal, the scope note, the next-turn sentence,
    /// or a shared clause — fails here.
    #[test]
    fn no_other_module_hand_writes_the_disposition_vocabulary() {
        let voices = DispositionVoices::default();
        let mut owned: Vec<(String, String)> = Vec::new();
        for disposition in EVERY_DISPOSITION {
            let Some(voice) = voices.voice(disposition) else {
                continue;
            };
            // Compare against the text after the `harness_action: ` key so a
            // site that reconstructs only the payload is caught too.
            let payload = voice
                .card_action
                .strip_prefix("harness_action: ")
                .unwrap_or(&voice.card_action);
            owned.push((format!("{disposition:?} card line"), payload.to_string()));
            if !voice.denied_guidance.is_empty() {
                owned.push((
                    format!("{disposition:?} refusal guidance"),
                    voice.denied_guidance.clone(),
                ));
            }
        }
        owned.push((
            "inferred provenance".into(),
            voices.provenance_inferred.clone(),
        ));
        owned.push(("policy provenance".into(), voices.provenance_policy.clone()));
        owned.push(("privacy clause".into(), voices.privacy.clone()));
        owned.push(("refusal clause".into(), voices.denied_privacy.clone()));
        owned.push(("discovery entry".into(), voices.discovery_hidden.clone()));
        owned.push(("discovery scope".into(), voices.discovery_scope.clone()));
        owned.push(("next-turn scope".into(), voices.next_turn_scope.clone()));
        for (name, source) in scanned_sites() {
            for (what, text) in &owned {
                assert!(!text.is_empty(), "{what} is empty");
                assert!(
                    !source.contains(text.as_str()),
                    "{name} hand-writes the {what}. This vocabulary has exactly one owner \
                     (disposition_voice.rs); read it from there."
                );
            }
        }
    }
}