Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -44,6 +45,17 @@ pub struct CapabilityManifest {
/// on guarantee, cost, latency, and evidence envelopes.
#[serde(default)]
pub bindings: Vec<CapabilityBinding>,
/// 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<BTreeSet<LlmTier>>,
/// 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<BTreeSet<GrammarKind>>,
}

/// Stable name for [`crate::ir::IRPrimitive`] variants (for manifest matching).
Expand Down
82 changes: 82 additions & 0 deletions src/dossier.rs
Original file line number Diff line number Diff line change
@@ -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<EvidenceRecord>,
pub alternatives: Vec<Candidate>,
pub summary: String,
pub cumulative_cost: CostEnvelope,
pub frontier_question: String,
pub known_ghosts: Vec<GhostRecord>,
}

/// 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<String>,
},
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
81 changes: 81 additions & 0 deletions src/tier.rs
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
42 changes: 42 additions & 0 deletions tests/tier_admission.rs
Original file line number Diff line number Diff line change
@@ -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
);
}
}