Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c3d0c38
feat(psychometric): posterior ESEM input gates with true-parameter RMSE
seonghobae Aug 13, 2026
39d40d2
Merge protected main into PR #49 while preserving psychometric input …
seonghobae Aug 14, 2026
3acbc81
test(psychometric): expose ALR geometry and Rubin overclaim gaps
seonghobae Aug 14, 2026
18b37cf
fix(psychometric): script honest geometry and posterior claims
seonghobae Aug 14, 2026
189f618
chore(ci): verify PR 49 scientific claim repair
seonghobae Aug 14, 2026
1abeb39
fix(ci): normalize repaired documentation whitespace
seonghobae Aug 14, 2026
c42a073
test(psychometric): require stable extreme plausible-value means
seonghobae Aug 14, 2026
24bac68
test(psychometric): cover zero-scale plausible-value means
seonghobae Aug 14, 2026
568276e
fix(psychometric): stabilize plausible-value aggregation
seonghobae Aug 14, 2026
317f4be
fix(psychometric): align repair precondition with current docs
seonghobae Aug 15, 2026
c19086a
fix(psychometric): align stability tests with honest point estimate API
seonghobae Aug 15, 2026
f828f2b
test(psychometric): satisfy strict float comparison lint
seonghobae Aug 15, 2026
b100419
fix(psychometric): narrow geometry and posterior claims
github-actions[bot] Aug 15, 2026
199ed6c
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 20, 2026
f60748d
test(psychometric): close indicator branch coverage
seonghobae Aug 20, 2026
88ed042
test(psychometric): cover estimator edge branches
seonghobae Aug 20, 2026
aa18de7
Merge remote-tracking branch 'origin/agent/psychometric-posterior-ese…
seonghobae Aug 20, 2026
b713010
test(psychometric): repair merged coverage test
seonghobae Aug 20, 2026
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
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ boundaries above remain the target modular MSA architecture.
| `tepp_simulation` | known-truth temporal/event data generation |
| `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics |
| `tepp_api` | versioned DTO, schema, and export contracts |
| `psychometric_core` | posterior-aware structural input gates and CPU `f64` loading point-estimate recovery |

No crate exposes placeholder production behavior in Task 1. This prevents an
empty façade from becoming a de facto public API before its invariants and tests
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration).
- `tepp_api` adaptive orchestration router (ADR 0010): versioned `direct`/`verify`/`committee`/`conductor`/`abstain` selection from CPU `f64` risk, ambiguity, evidence, and token-budget inputs; recorded stages, recursion, decomposition, access lists, and role-specific reasoning effort; fail-closed document-controlled policy/access/credentials; LLM plans remain proposals under deterministic statistical authority; comparable-budget ablation requires a direct baseline; credential-free contextual-orchestrator binding. Live NIM HTTP remains accepted-target.
- `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure.
- `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013).
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/psychometric_core",
]
default-members = [
"crates/evidence_core",
Expand All @@ -23,6 +24,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/psychometric_core",
]

[workspace.package]
Expand Down
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Hourly NIM product-development operations | [`docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md`](docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md) |
| Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) |
| Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) |
| Posterior ESEM/DSEM input-gate doctoring | [`docs/research/posterior-esem-input-gates.md`](docs/research/posterior-esem-input-gates.md) |
| Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) |
| Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) |
| Adaptive orchestration router doctoring | [`docs/research/adaptive-orchestration-router.md`](docs/research/adaptive-orchestration-router.md) |
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ implemented in Rust.
## Current implementation state

This branch establishes the Task 1 Rust workspace and quality-gate foundation.
The ten bounded crates compile independently but intentionally expose no
placeholder production APIs. Domain behavior begins in Task 2 with immutable
evidence identifiers and source records.
The eleven bounded crates compile independently. Domain crates expose only
validated production APIs; placeholder surfaces are prohibited.

```text
crates/evidence_core
Expand All @@ -22,6 +21,7 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/psychometric_core # construct/input gates; not a full ESEM/DSEM estimator
```

## Local verification
Expand Down
19 changes: 19 additions & 0 deletions crates/psychometric_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "psychometric_core"
description = "Posterior-aware ESEM/DSEM input gates and CPU f64 loading recovery."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
publish = false

[dependencies]

[lints]
workspace = true
65 changes: 65 additions & 0 deletions crates/psychometric_core/src/causality.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Refusal of causal language from non-identifying heuristics.

use crate::error::PsychometricError;

/// A heuristic that is not, by itself, causal identification.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CausalHeuristic {
/// Event-time or document-time precedence.
TemporalPrecedence,
/// A citation, revision, or other document link.
DocumentLinkage,
/// TDT-style event tracking or coreference.
EventTracking,
/// A model prediction or schema completion.
ModelPrediction,
}

impl CausalHeuristic {
/// Stable wire name for the heuristic.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TemporalPrecedence => "temporal_precedence",
Self::DocumentLinkage => "document_linkage",
Self::EventTracking => "event_tracking",
Self::ModelPrediction => "model_prediction",
}
}
}

/// Refuse a causal-effect claim that rests only on a non-identifying heuristic.
///
/// ADR 0005: temporal precedence, document linkage, event tracking, or model
/// prediction alone do not justify causal language.
///
/// # Errors
///
/// Always returns [`PsychometricError::CausalUnderidentified`].
pub fn claim_causal_effect(_heuristic: CausalHeuristic) -> Result<(), PsychometricError> {
Err(PsychometricError::CausalUnderidentified)
}

#[cfg(test)]
mod tests {
use super::{CausalHeuristic, claim_causal_effect};
use crate::error::PsychometricError;

#[test]
fn every_heuristic_is_underidentified() {
assert_eq!(
claim_causal_effect(CausalHeuristic::DocumentLinkage),
Err(PsychometricError::CausalUnderidentified)
);
assert_eq!(
CausalHeuristic::TemporalPrecedence.as_str(),
"temporal_precedence"
);
assert_eq!(CausalHeuristic::EventTracking.as_str(), "event_tracking");
assert_eq!(
CausalHeuristic::ModelPrediction.as_str(),
"model_prediction"
);
}
}
95 changes: 95 additions & 0 deletions crates/psychometric_core/src/construct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Construct-class classification and interpretation gates.

use crate::error::PsychometricError;

/// Higher-order construct class before ESEM, composite, or network modeling.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ConstructClass {
/// Reflective indicators of a common latent factor.
Reflective,
/// Formative or composite indicators that define the construct.
Formative,
/// Interacting indicators that belong in a network model.
Network,
/// Insufficient evidence to classify the construct.
Unresolved,
}

impl ConstructClass {
/// Stable wire name for the construct class.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Reflective => "reflective",
Self::Formative => "formative",
Self::Network => "network",
Self::Unresolved => "unresolved",
}
}

/// Return whether reflective ESEM/set-ESEM is admissible.
#[must_use]
pub const fn admits_reflective_esem(self) -> bool {
matches!(self, Self::Reflective)
}
}

/// Interpret a classified construct as reflective.
///
/// A good global fit statistic is not authority to reinterpret a formative or
/// network structure as reflective (ADR 0005).
///
/// # Errors
///
/// Returns [`PsychometricError::FormativeReinterpretationForbidden`] for
/// formative or network classes and
/// [`PsychometricError::UnresolvedConstruct`] when the class is unresolved.
pub fn interpret_as_reflective(
classified: ConstructClass,
global_fit_acceptable: bool,
) -> Result<ConstructClass, PsychometricError> {
match (classified, global_fit_acceptable) {
(ConstructClass::Reflective, true | false) => Ok(ConstructClass::Reflective),
(ConstructClass::Unresolved, true | false) => Err(PsychometricError::UnresolvedConstruct),
(ConstructClass::Formative | ConstructClass::Network, true | false) => {
Err(PsychometricError::FormativeReinterpretationForbidden)
}
}
}

/// Permit a latent-mean or path comparison only when invariance evidence is
/// already established for the claimed comparison.
///
/// # Errors
///
/// Returns [`PsychometricError::InvarianceRequired`] when the required
/// invariance level has not been met.
pub fn compare_latent_means(invariance_level_met: bool) -> Result<(), PsychometricError> {
if invariance_level_met {
Ok(())
} else {
Err(PsychometricError::InvarianceRequired)
}
}

#[cfg(test)]
mod tests {
use super::{ConstructClass, compare_latent_means, interpret_as_reflective};
use crate::error::PsychometricError;

#[test]
fn reflective_only_admits_esem_and_invariance_is_required() {
assert!(ConstructClass::Reflective.admits_reflective_esem());
assert!(!ConstructClass::Formative.admits_reflective_esem());
compare_latent_means(true).expect("ok");
assert_eq!(
interpret_as_reflective(ConstructClass::Reflective, true).expect("fit unused"),
ConstructClass::Reflective
);
assert_eq!(
interpret_as_reflective(ConstructClass::Network, false),
Err(PsychometricError::FormativeReinterpretationForbidden)
);
}
}
85 changes: 85 additions & 0 deletions crates/psychometric_core/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Fail-closed psychometric input and recovery errors.

use std::fmt;

/// A fail-closed psychometric-domain error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PsychometricError {
/// Raw simplex proportions were offered as Euclidean indicators.
RawProportionForbidden,
/// Empty, unequal-length, or non-finite numeric input.
InvalidNumericInput,
/// A predictor or indicator vector has zero variance.
SingularDesign,
/// A good global fit was used to reinterpret a formative or network
/// construct as reflective.
FormativeReinterpretationForbidden,
/// Temporal precedence, linkage, tracking, or prediction was treated as
/// causal identification.
CausalUnderidentified,
/// The construct class is unresolved and cannot support a reflective
/// interpretation.
UnresolvedConstruct,
/// Latent-mean or path comparison was requested without invariance
/// evidence.
InvarianceRequired,
}

impl fmt::Display for PsychometricError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::RawProportionForbidden => {
"raw topic proportions are forbidden psychometric indicators"
}
Self::InvalidNumericInput => "invalid psychometric numeric input",
Self::SingularDesign => "singular psychometric design matrix",
Self::FormativeReinterpretationForbidden => {
"formative or network constructs cannot be reinterpreted as reflective"
}
Self::CausalUnderidentified => "temporal precedence is not causal identification",
Self::UnresolvedConstruct => "construct class is unresolved",
Self::InvarianceRequired => "latent-mean comparison requires invariance evidence",
};
formatter.write_str(message)
}
}

impl std::error::Error for PsychometricError {}

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

#[test]
fn messages_are_stable() {
assert_eq!(
PsychometricError::RawProportionForbidden.to_string(),
"raw topic proportions are forbidden psychometric indicators"
);
assert_eq!(
PsychometricError::InvalidNumericInput.to_string(),
"invalid psychometric numeric input"
);
assert_eq!(
PsychometricError::SingularDesign.to_string(),
"singular psychometric design matrix"
);
assert_eq!(
PsychometricError::FormativeReinterpretationForbidden.to_string(),
"formative or network constructs cannot be reinterpreted as reflective"
);
assert_eq!(
PsychometricError::CausalUnderidentified.to_string(),
"temporal precedence is not causal identification"
);
assert_eq!(
PsychometricError::UnresolvedConstruct.to_string(),
"construct class is unresolved"
);
assert_eq!(
PsychometricError::InvarianceRequired.to_string(),
"latent-mean comparison requires invariance evidence"
);
}
}
Loading
Loading