From 224fd991a985fa23e368fda65222b58c360fe293 Mon Sep 17 00:00:00 2001 From: danvoulez Date: Mon, 18 May 2026 13:58:18 +0100 Subject: [PATCH] Add LLM tier and dossier primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LlmTier - Add GrammarKind - Add Dossier - Add FrontierVerdict - Extend CapabilityManifest with allowed ingress tiers and grammars - Add tier/grammar matrix tests Tracks LIP-0008 (Proposed). Does not implement LIP-0008. Makes LIP-0008 representable in the type system. Option> on the new CapabilityManifest fields preserves the distinction between "legacy / unspecified" (None) and "explicitly allows nothing" (Some(empty)), so later admission patches can route each case correctly. Test: exhaustive 3 × 4 matrix on GrammarKind::admits. Lib tests unchanged (141/141, no regression). Co-Authored-By: Claude Opus 4.7 --- src/capability.rs | 12 ++++++ src/dossier.rs | 82 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ src/tier.rs | 81 ++++++++++++++++++++++++++++++++++++++++ tests/tier_admission.rs | 42 +++++++++++++++++++++ 5 files changed, 221 insertions(+) create mode 100644 src/dossier.rs create mode 100644 src/tier.rs create mode 100644 tests/tier_admission.rs diff --git a/src/capability.rs b/src/capability.rs index 6a45d87..7c371a1 100644 --- a/src/capability.rs +++ b/src/capability.rs @@ -18,6 +18,7 @@ use crate::act_identity::CanonicalActionId; use crate::ir::IRPrimitive; +use crate::tier::{GrammarKind, LlmTier}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -44,6 +45,17 @@ pub struct CapabilityManifest { /// on guarantee, cost, latency, and evidence envelopes. #[serde(default)] pub bindings: Vec, + /// LIP-0008: which LLM tiers this substrate accepts at ingress. + /// `None` = unspecified (legacy / compat); `Some(empty)` = explicitly + /// allows zero tiers; `Some(set)` = explicit policy. The distinction + /// matters when admission enforcement lands in a later patch. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub allowed_ingress_tiers: Option>, + /// LIP-0008: which grammars this substrate accepts at ingress. + /// Same `None` / `Some(empty)` / `Some(set)` semantics as + /// `allowed_ingress_tiers`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub allowed_grammars: Option>, } /// Stable name for [`crate::ir::IRPrimitive`] variants (for manifest matching). diff --git a/src/dossier.rs b/src/dossier.rs new file mode 100644 index 0000000..f81becb --- /dev/null +++ b/src/dossier.rs @@ -0,0 +1,82 @@ +//! Dossier: the only admissible shape crossing the Frontier boundary (LIP-0008). +//! +//! A [`Dossier`] is assembled bottom-up by lower tiers (Mini, Operator, +//! Translator) before any Frontier call. The Frontier never sees raw user +//! input, unbounded context, or chat history — only the prepared case. +//! The Frontier's response, [`FrontierVerdict`], is bounded (yes/no with +//! reason) and is itself a content-citable record; it is **never** used +//! as evidence closure on its own. +//! +//! This module introduces the types; admission, dispatch, and +//! Frontier-side wiring belong to later patches. +//! +//! See `LogLine-Foundation/governance/lips/LIP-0008-llm-tier-discipline-and-dossier-discipline.md`. + +use serde::{Deserialize, Serialize}; + +use crate::capability::CostEnvelope; +use crate::evidence::EvidenceRecord; + +/// A bounded decision the Frontier is asked to rule on. The institution +/// frames the question; the Frontier does not invent it. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct DecisionRequest { + pub action_id: String, + pub scope: String, +} + +/// A candidate already prepared by lower tiers. The Frontier picks among +/// or ratifies one of them — it does not author candidates. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Candidate { + pub candidate_id: String, + pub summary: String, +} + +/// A structured absence observed by the pipeline before the dossier was +/// assembled. Surfaced to the Frontier so its verdict is informed by +/// what was NOT resolvable lower in the stack. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GhostRecord { + pub ghost_id: String, + pub kind: String, + pub reason: String, +} + +/// The only admissible shape crossing the Frontier boundary. +/// +/// Forbidden inputs to the Frontier (never modeled here, and rejected by +/// admission later): raw chat history, unbounded workspace context, +/// entire repo dumps, ambiguous "please solve this", provider miracle +/// prompts. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Dossier { + pub dossier_id: String, + pub target_decision: DecisionRequest, + pub evidence_chain: Vec, + pub alternatives: Vec, + pub summary: String, + pub cumulative_cost: CostEnvelope, + pub frontier_question: String, + pub known_ghosts: Vec, +} + +/// The Frontier's bounded verdict on a [`Dossier`]. +/// +/// Yes/No, each with a reason. `signed_at` on `Yes` records when the +/// verdict was bound (ISO-8601 string at this stage; later patches may +/// tighten this to a canonical receipt id). `alternative_suggestion` +/// on `No` is optional and informational only — it is not authority. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "verdict")] +pub enum FrontierVerdict { + Yes { + reason: String, + signed_at: String, + }, + No { + reason: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + alternative_suggestion: Option, + }, +} diff --git a/src/lib.rs b/src/lib.rs index e000ec0..ca2cbf3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod act_identity; pub mod admission; pub mod capability; pub mod decision; +pub mod dossier; pub mod evidence; #[cfg(feature = "sqlite-evidence")] pub mod evidence_sqlite; @@ -25,6 +26,7 @@ pub mod planning_compiler; pub mod policy; pub mod refs; pub mod strong_grammar; +pub mod tier; pub mod validation; pub use act_identity::{CanonicalActionId, IdentityError}; @@ -40,6 +42,7 @@ pub use decision::{ assert_decide_free, compile_flow, compile_node, contains_decide, lower_compiled_flow, materialize_primitive, resolve_lower_one, DecideResolver, PlannerError, PlannerLoweringError, }; +pub use dossier::{Candidate, DecisionRequest, Dossier, FrontierVerdict, GhostRecord}; pub use evidence::{ close_execution_evidence, EvidenceContract, EvidenceRecord, EvidenceStore, EvidenceStoreError, FailureToClose, FileEvidenceStore, @@ -72,6 +75,7 @@ pub use strong_grammar::{ compile_strong_json_to_ir_graph, compile_strong_program, parse_strong_json, ConfirmSpec, ExecuteSpec, PipelineSpec, PipelineStep, ReviewSpec, StrongHandler, StrongProgram, }; +pub use tier::{GrammarKind, LlmTier}; pub use validation::{ check_capability, validate_admissibility, validate_capability, validate_policy, validate_structure, AdmissibilityContext, AdmissibleNode, ValidationError, diff --git a/src/tier.rs b/src/tier.rs new file mode 100644 index 0000000..5e88e2e --- /dev/null +++ b/src/tier.rs @@ -0,0 +1,81 @@ +//! LLM tier and grammar discipline (LIP-0008). +//! +//! Constitutional rules for how LLMs participate in the pipeline. +//! Four tiers ([`LlmTier::Mini`], [`LlmTier::Operator`], [`LlmTier::Translator`], +//! [`LlmTier::Frontier`]) and three grammars ([`GrammarKind::Operational`], +//! [`GrammarKind::Strong`], [`GrammarKind::Dossier`]). Each tier carries the +//! smallest grammar it can honestly emit; raising tier is an efficiency +//! failure, not a capability badge. +//! +//! These types make LIP-0008 representable in the type system. They do not +//! implement admission enforcement — that belongs to a later patch that +//! teaches `admission` and the planning compiler to consult tier × grammar. +//! +//! See `LogLine-Foundation/governance/lips/LIP-0008-llm-tier-discipline-and-dossier-discipline.md`. + +use serde::{Deserialize, Serialize}; + +/// The four LLM tiers. +/// +/// Ordering reflects typical model size and call-cost envelope, not +/// authority — no tier can close evidence or authorize material execution. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LlmTier { + /// 1.5–3.5B (or hot local 7B). Classify, extract, mark uncertainty, + /// propose tiny candidate fragments. Sustained 24/7 call pattern. + Mini, + /// 9–14B local. Conduct session, decompose goals, route workorders. + Operator, + /// 9–14B local, escalates when needed. Natural language → LogLine + /// candidate. + Translator, + /// External API. Receives a prepared [`crate::Dossier`] and returns a + /// bounded verdict. Called rarely. + Frontier, +} + +/// The three grammars admissible at ingress boundaries. +/// +/// [`GrammarKind::Operational`] is the runtime's line-oriented surface +/// grammar (see [`crate::operational_grammar`]). [`GrammarKind::Strong`] +/// is the JSON canonical IR ingress (see [`crate::strong_grammar`]). +/// [`GrammarKind::Dossier`] is the only shape that crosses the Frontier +/// boundary (see [`crate::Dossier`]). +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum GrammarKind { + /// Line-oriented surface grammar. Mini and Operator tiers. + Operational, + /// JSON canonical IR ingress. Operator and Translator tiers. + Strong, + /// Prepared dossier with evidence chain and bounded question. + /// Frontier tier only. + Dossier, +} + +impl GrammarKind { + /// Constitutional rule: which tiers may legitimately emit via this + /// grammar. + /// + /// This is the matrix from LIP-0008 §5. The per-substrate admission + /// (whether a particular [`crate::CapabilityManifest`] accepts the + /// pair) is enforced separately in admission/planning code. + /// + /// ``` + /// use constitutional_runtime::{GrammarKind, LlmTier}; + /// assert!(GrammarKind::Operational.admits(&LlmTier::Mini)); + /// assert!(GrammarKind::Dossier.admits(&LlmTier::Frontier)); + /// assert!(!GrammarKind::Operational.admits(&LlmTier::Frontier)); + /// ``` + pub fn admits(&self, tier: &LlmTier) -> bool { + matches!( + (self, tier), + (GrammarKind::Operational, LlmTier::Mini) + | (GrammarKind::Operational, LlmTier::Operator) + | (GrammarKind::Strong, LlmTier::Operator) + | (GrammarKind::Strong, LlmTier::Translator) + | (GrammarKind::Dossier, LlmTier::Frontier) + ) + } +} diff --git a/tests/tier_admission.rs b/tests/tier_admission.rs new file mode 100644 index 0000000..2ebe429 --- /dev/null +++ b/tests/tier_admission.rs @@ -0,0 +1,42 @@ +//! LIP-0008 tier × grammar admission matrix. +//! +//! Exhaustive 3 × 4 = 12 cell matrix asserting the constitutional rule +//! encoded in `GrammarKind::admits`. Per-substrate admission (i.e. whether +//! a particular `CapabilityManifest` accepts the pair) is enforced +//! elsewhere and is not exercised here. + +use constitutional_runtime::{GrammarKind, LlmTier}; + +#[test] +fn tier_grammar_admission_matrix() { + use GrammarKind::*; + use LlmTier::*; + + // (grammar, tier, expected_admits) + let matrix: &[(GrammarKind, LlmTier, bool)] = &[ + // Operational ingress: Mini and Operator only. + (Operational, Mini, true), + (Operational, Operator, true), + (Operational, Translator, false), + (Operational, Frontier, false), + // Strong ingress: Operator and Translator only. + (Strong, Mini, false), + (Strong, Operator, true), + (Strong, Translator, true), + (Strong, Frontier, false), + // Dossier ingress: Frontier only. + (Dossier, Mini, false), + (Dossier, Operator, false), + (Dossier, Translator, false), + (Dossier, Frontier, true), + ]; + + for (grammar, tier, expected) in matrix { + let got = grammar.admits(tier); + assert_eq!( + got, *expected, + "GrammarKind::{:?}.admits(LlmTier::{:?}) = {}, expected {}", + grammar, tier, got, expected + ); + } +}