Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9701b3d
feat(topic): logistic-normal ALR coordinates with true-parameter RMSE
seonghobae Aug 13, 2026
130bc71
test(topic): expose unstable ALR overflow path
seonghobae Aug 14, 2026
e76a0de
test(topic): reject inverse underflow to a zero simplex part
seonghobae Aug 14, 2026
30ae9e0
fix(topic): script stable ALR and preserve shared ledgers
seonghobae Aug 14, 2026
bce4ac6
chore(ci): verify PR 48 stable log-ratio repair
seonghobae Aug 14, 2026
c95ebe1
fix(topic): stabilize extreme log-ratio coordinates
github-actions[bot] Aug 14, 2026
5106f4b
docs(topic): specify stable inverse reference coordinate
seonghobae Aug 14, 2026
0269df0
test(topic): detect hidden simplex mass with compensated summation
seonghobae Aug 14, 2026
d70f5c7
fix(topic): validate simplex mass with compensated summation
seonghobae Aug 14, 2026
0c3d88d
test(topic): keep compensated-sum fixture clippy-clean
seonghobae Aug 14, 2026
0d7c65f
style(topic): apply pinned rustfmt output
seonghobae Aug 14, 2026
f3540fa
test(quality): include topic measurement in docstring inventory
seonghobae Aug 14, 2026
ab29247
test(topic): cover non-finite compensated simplex mass
cursoragent Aug 17, 2026
4ae9b5d
feat(topic): add sequential Egozcue ILR coordinates
cursoragent Aug 17, 2026
a52dd55
test(topic): hit non-finite ILR centered-log reconstruction
cursoragent Aug 17, 2026
30c1d1f
test(topic): recover three-part sequential ILR in unit tests
cursoragent Aug 18, 2026
2f08cdd
docs(adr): align 0010 and 0013 maturity with merged main
cursoragent Aug 18, 2026
6fe6a78
chore(docs): remove trailing whitespace from ADR maturity lines
seonghobae Aug 19, 2026
d1f3dc3
fix(coverage): ignore Rust multiline string fragments
seonghobae Aug 19, 2026
4b54dc8
test(topic): assert direct Aitchison distance recovery
seonghobae Aug 20, 2026
543982c
fix(coverage): parse Rust strings and comments statefully
seonghobae Aug 20, 2026
621cae0
docs: record stateful coverage parser repair
seonghobae Aug 20, 2026
8e88b33
docs(topic): define ILR pairwise distance
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 |
| `topic_measurement` | logistic-normal ALR and sequential Egozcue ILR topic coordinates |

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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `topic_measurement` logistic-normal additive log-ratio and sequential Egozcue isometric log-ratio coordinates: fail-closed simplex validation, max-shifted stable ALR/ILR inverses with true-parameter RMSE, Aitchison-distance ILR isometry, and refusal of TF-IDF/BM25/keyword scores as inferential topic coordinates (ADR 0012 first production slice; no new migration).
- Coverage contract now excludes Rust multiline string continuation records emitted by LLVM LCOV, keeping the 100% authored-line gate focused on executable production lines.
- Coverage source classification now scans Rust strings and comments with escape-parity state, preserving executable string method calls and ignoring quoted comments.
- `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/topic_measurement",
]
default-members = [
"crates/evidence_core",
Expand All @@ -23,6 +24,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/topic_measurement",
]

[workspace.package]
Expand Down
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| 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) |
| Topic log-ratio coordinate doctoring | [`docs/research/topic-logratio-coordinates.md`](docs/research/topic-logratio-coordinates.md) |
| Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) |
| Change history | [`CHANGELOG.md`](CHANGELOG.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/topic_measurement
```

## Local verification
Expand Down
17 changes: 17 additions & 0 deletions crates/topic_measurement/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "topic_measurement"
description = "Logistic-normal and log-ratio coordinates for compositional topics."
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

[lints]
workspace = true
222 changes: 222 additions & 0 deletions crates/topic_measurement/src/coordinates.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//! Additive and isometric log-ratio maps for compositional topic coordinates.

use crate::error::TopicMeasurementError;

const UNIT_SUM_TOLERANCE: f64 = 1e-12;

/// Map a strictly positive unit simplex vector to additive log-ratio coordinates.
///
/// For a `K`-part composition `θ` the image is the `K-1` vector
/// `y_k = ln(θ_k / θ_K)`. This reference-dependent, full-rank coordinate map
/// supports logistic-normal regression and ESEM/DSEM interfaces. It is not an
/// orthonormal isometry for Aitchison distance; use ILR coordinates when that
/// Euclidean geometry is the estimand.
///
/// # Errors
///
/// Returns [`TopicMeasurementError::InvalidComposition`] when the vector is
/// empty, has fewer than two parts, contains a non-finite or non-positive
/// entry, or does not sum to one within a tight absolute tolerance.
pub fn additive_log_ratio(proportions: &[f64]) -> Result<Vec<f64>, TopicMeasurementError> {
let last = require_composition(proportions)?;
let reference_log = last.ln();
Ok(proportions[..proportions.len() - 1]
.iter()
.map(|part| part.ln() - reference_log)
.collect())
}

/// Invert additive log-ratio coordinates back to the unit simplex.
///
/// # Errors
///
/// Returns [`TopicMeasurementError::InvalidLogRatioDimension`] when the
/// coordinate vector is empty, non-finite, or would underflow a part to zero
/// in the strictly positive `f64` simplex representation.
pub fn from_additive_log_ratio(coordinates: &[f64]) -> Result<Vec<f64>, TopicMeasurementError> {
if coordinates.is_empty() {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}
let mut maximum = 0.0_f64;
for &value in coordinates {
if !value.is_finite() {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}
maximum = maximum.max(value);
}

let reference_weight = (-maximum).exp();
if reference_weight == 0.0 {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}
let mut shifted_weights = Vec::with_capacity(coordinates.len());
let mut denominator = reference_weight;
for &value in coordinates {
let weight = (value - maximum).exp();
if weight == 0.0 {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}
denominator += weight;
shifted_weights.push(weight);
}

let mut simplex = Vec::with_capacity(coordinates.len() + 1);
for weight in shifted_weights {
simplex.push(weight / denominator);
}
simplex.push(reference_weight / denominator);
Ok(simplex)
}

/// Map a strictly positive unit simplex vector to isometric log-ratio coordinates.
///
/// The sequential Egozcue orthonormal basis sends a `K`-part composition to
/// the `K-1` vector whose Euclidean distance from another composition's ILR
/// vector equals their Aitchison distance. A vector norm is only the distance
/// from the equal-share origin. This is the coordinate system for distance-based
/// topic geometry. It is not the reference-dependent logistic-normal map; use
/// [`additive_log_ratio`] when that regression interface is the estimand.
///
/// # Errors
///
/// Returns [`TopicMeasurementError::InvalidComposition`] when the vector is
/// empty, has fewer than two parts, contains a non-finite or non-positive
/// entry, or does not sum to one within a tight absolute tolerance.
pub fn isometric_log_ratio(proportions: &[f64]) -> Result<Vec<f64>, TopicMeasurementError> {
require_composition(proportions)?;
let dimension = proportions.len();
let logs: Vec<f64> = proportions.iter().map(|part| part.ln()).collect();
let mut coordinates = Vec::with_capacity(dimension - 1);
for index in 0..(dimension - 1) {
let remaining = dimension - index - 1;
#[allow(clippy::cast_precision_loss)]
let remaining_f = remaining as f64;
let scale = (remaining_f / (remaining_f + 1.0)).sqrt();
let mut rest_sum = 0.0_f64;
for log_part in &logs[index + 1..] {
rest_sum += *log_part;
}
coordinates.push(scale * (logs[index] - rest_sum / remaining_f));
}
Ok(coordinates)
}

/// Invert isometric log-ratio coordinates back to the unit simplex.
///
/// # Errors
///
/// Returns [`TopicMeasurementError::InvalidLogRatioDimension`] when the
/// coordinate vector is empty, non-finite, or would underflow a part to zero
/// in the strictly positive `f64` simplex representation.
pub fn from_isometric_log_ratio(coordinates: &[f64]) -> Result<Vec<f64>, TopicMeasurementError> {
if coordinates.is_empty() {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}
for &value in coordinates {
if !value.is_finite() {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}
}

let dimension = coordinates.len() + 1;
let mut centered_logs = vec![0.0_f64; dimension];
for (index, &coordinate) in coordinates.iter().enumerate() {
let remaining = dimension - index - 1;
#[allow(clippy::cast_precision_loss)]
let remaining_f = remaining as f64;
let scale = (remaining_f / (remaining_f + 1.0)).sqrt();
let negative = -1.0 / (remaining_f * (remaining_f + 1.0)).sqrt();
centered_logs[index] += scale * coordinate;
for centered in &mut centered_logs[index + 1..] {
*centered += negative * coordinate;
}
}

let mut maximum = centered_logs[0];
for &value in &centered_logs[1..] {
maximum = maximum.max(value);
}
if !maximum.is_finite() {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}

let mut weights = Vec::with_capacity(dimension);
let mut denominator = 0.0_f64;
for &value in &centered_logs {
let weight = (value - maximum).exp();
if weight == 0.0 {
return Err(TopicMeasurementError::InvalidLogRatioDimension);
}
denominator += weight;
weights.push(weight);
}
Ok(weights.iter().map(|weight| weight / denominator).collect())
}

fn require_composition(proportions: &[f64]) -> Result<f64, TopicMeasurementError> {
if proportions.len() < 2 {
return Err(TopicMeasurementError::InvalidComposition);
}
let mut sum = 0.0_f64;
let mut compensation = 0.0_f64;
for &part in proportions {
if !part.is_finite() || part <= 0.0 {
return Err(TopicMeasurementError::InvalidComposition);
}
let next = sum + part;
compensation += if sum.abs() >= part.abs() {
(sum - next) + part
} else {
(part - next) + sum
};
sum = next;
}
let compensated_sum = sum + compensation;
if !compensated_sum.is_finite() || (compensated_sum - 1.0).abs() > UNIT_SUM_TOLERANCE {
return Err(TopicMeasurementError::InvalidComposition);
}
Ok(proportions[proportions.len() - 1])
}

#[cfg(test)]
mod tests {
use super::{
additive_log_ratio, from_additive_log_ratio, from_isometric_log_ratio, isometric_log_ratio,
};
use crate::error::TopicMeasurementError;

#[test]
fn two_part_equal_shares_are_zero_and_unrepresentable_extremes_fail_closed() {
let pair = additive_log_ratio(&[0.5, 0.5]).expect("pair");
assert_eq!(pair.len(), 1);
assert!(pair[0].abs() < 1e-15);
let recovered = from_additive_log_ratio(&pair).expect("inverse");
assert!((recovered[0] - 0.5).abs() < 1e-15);
assert!((recovered[1] - 0.5).abs() < 1e-15);
assert_eq!(
from_additive_log_ratio(&[1.0e9]),
Err(TopicMeasurementError::InvalidLogRatioDimension)
);
assert_eq!(
additive_log_ratio(&[f64::MAX, f64::MAX]),
Err(TopicMeasurementError::InvalidComposition),
"overflowing finite parts must fail closed because compensated mass is non-finite"
);
let origin = isometric_log_ratio(&[0.5, 0.5]).expect("ilr origin");
assert!(origin[0].abs() < 1e-15);
let recovered_ilr = from_isometric_log_ratio(&origin).expect("ilr inverse");
assert!((recovered_ilr[0] - 0.5).abs() < 1e-15);
assert_eq!(
from_isometric_log_ratio(&[1000.0]),
Err(TopicMeasurementError::InvalidLogRatioDimension)
);
assert_eq!(
from_isometric_log_ratio(&[-f64::MAX, f64::MAX]),
Err(TopicMeasurementError::InvalidLogRatioDimension)
);
let three = isometric_log_ratio(&[2.0 / 6.0, 3.0 / 6.0, 1.0 / 6.0]).expect("ilr three");
assert!((three[1] - (0.5_f64).sqrt() * 3.0_f64.ln()).abs() < 1e-15);
let recovered_three = from_isometric_log_ratio(&three).expect("ilr three inverse");
assert!((recovered_three.iter().sum::<f64>() - 1.0).abs() < 1e-15);
}
}
50 changes: 50 additions & 0 deletions crates/topic_measurement/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Fail-closed topic-coordinate errors.

use std::fmt;

/// A fail-closed topic-measurement error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TopicMeasurementError {
/// Composition is empty, has fewer than two parts, is non-positive,
/// non-finite, or does not sum to one.
InvalidComposition,
/// Log-ratio vector is empty, non-finite, or not representable as a strictly positive `f64` simplex.
InvalidLogRatioDimension,
/// TF-IDF, BM25, or keyword scores were offered as inferential coordinates.
LexicalWeightForbidden,
}

impl fmt::Display for TopicMeasurementError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::InvalidComposition => "invalid compositional topic vector",
Self::InvalidLogRatioDimension => "invalid log-ratio dimension",
Self::LexicalWeightForbidden => "lexical inferential weights are forbidden",
};
formatter.write_str(message)
}
}

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

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

#[test]
fn messages_are_stable() {
assert_eq!(
TopicMeasurementError::InvalidComposition.to_string(),
"invalid compositional topic vector"
);
assert_eq!(
TopicMeasurementError::InvalidLogRatioDimension.to_string(),
"invalid log-ratio dimension"
);
assert_eq!(
TopicMeasurementError::LexicalWeightForbidden.to_string(),
"lexical inferential weights are forbidden"
);
}
}
Loading
Loading