Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ae16463
chore: record implementation plan
Copilot Oct 19, 2026
d889ccd
simplify(lcm): type provided summary metadata
Copilot Oct 19, 2026
ef504f6
refactor(lcm): type compression policy and outcomes
Copilot Oct 19, 2026
247a385
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 20, 2026
f9cd73a
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 20, 2026
e43b2c6
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 20, 2026
30162bd
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 21, 2026
d89428e
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 21, 2026
e5aae67
fix(lcm): preserve completed pressure recovery outcomes
ScriptedAlchemy Oct 21, 2026
ce7a6f3
fix(lcm): complete typed summary caller cutover
ScriptedAlchemy Oct 21, 2026
e096648
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 21, 2026
170f72b
style(lcm): use shorthand for compression overrides
ScriptedAlchemy Oct 21, 2026
961b873
Merge remote-tracking branch 'origin/master' into copilot/simplify-lc…
ScriptedAlchemy Oct 21, 2026
a3d1c10
fix(lcm): reject unknown compression completion statuses
ScriptedAlchemy Oct 21, 2026
82e426e
fix(lcm): finish typed summary input and compression outcomes
ScriptedAlchemy Oct 20, 2026
12b6b41
simplify(session-runtime): give LCM authority inherent store methods
ScriptedAlchemy Oct 20, 2026
5537e07
fix(lcm): guard typed summary deferrals
ScriptedAlchemy Oct 21, 2026
cb664cc
fix(lcm): restrict pressure completion states
ScriptedAlchemy Oct 21, 2026
00460a9
fix(lcm): reject unknown completion statuses
ScriptedAlchemy Oct 21, 2026
5e38df6
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 21, 2026
8b71167
fix(lcm): restore response import and direct deferral return
ScriptedAlchemy Oct 21, 2026
37e2160
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 21, 2026
7b96709
test(lcm): align compression fixtures with canonical summaries
ScriptedAlchemy Oct 21, 2026
c4fd88b
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 21, 2026
6c70575
test(index): settle graph admission before pinning save baseline
ScriptedAlchemy Oct 21, 2026
1ed4a0a
Merge branch 'master' into copilot/simplify-lcm-replace-json-in-route…
ScriptedAlchemy Oct 21, 2026
8290d8f
Merge master into LCM summary policy work
ScriptedAlchemy Oct 22, 2026
77b01f0
fix(lsp): observe analyzer exit after transport EOF
ScriptedAlchemy Oct 22, 2026
12efe5d
test(registry): scope non-UTF-8 filesystem fixture to Linux
ScriptedAlchemy Oct 22, 2026
121523d
docs(lsp): keep the exit observation invariant concise
ScriptedAlchemy Oct 22, 2026
223182b
fix(db): batch authority trigger validation
ScriptedAlchemy Oct 22, 2026
93c0848
test(hooks): settle fixture owners before removing inputs
ScriptedAlchemy Oct 22, 2026
62ba8a5
docs(db): remove trigger helper narration
ScriptedAlchemy Oct 22, 2026
21cb484
fix(lsp): release cancelled semantic request identities
ScriptedAlchemy Oct 22, 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
7 changes: 4 additions & 3 deletions crates/tracedecay-global-db/src/project_registry/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

use std::cell::Cell;
use std::collections::HashSet;
#[cfg(unix)]
#[cfg(target_os = "linux")]
use std::ffi::OsString;
#[cfg(unix)]
#[cfg(target_os = "linux")]
use std::os::unix::ffi::OsStringExt as _;
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -971,7 +971,8 @@ async fn anchors_do_not_keep_a_missing_project_alive_but_its_store_does() {
);
}

#[cfg(unix)]
// APFS rejects the non-UTF-8 directory name before registry admission.
#[cfg(target_os = "linux")]
#[asupersync::test]
async fn native_path_aliases_protect_roots_from_lossy_registry_reaping() {
let harness = RegisteredGlobalDbHarness::open("project-registry-native-reap").await;
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay-global-db/src/registered_lcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ impl RegisteredGlobalDb {
tracing::trace_span!("sessions.lcm.compress"),
);
let bounded = compressed.await?;
if bounded.response.status != "needs_summary"
if bounded.response.outcome()? == tracedecay_lcm::LcmCompressionOutcome::Complete
&& let Some(candidate) = convergence_candidate
{
let state = if bounded.has_more {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use rows::{
validate_source_cursor_authority_rows,
};
use triggers::{FOREIGN_KEY_AUDIT_QUERY, replace_trigger, trigger_contracts_intact};
pub(super) use triggers::{INVARIANTS, Trigger};
pub(super) use triggers::{INVARIANTS, first_invalid_trigger};
const OPERATION: &str = "ensure global database authority invariants";
const INCOMPLETE_EXHAUSTIVE_PASS: i64 = -1;
const FOREIGN_KEY_AUDIT_PROGRESS: &str = "authority-invariants";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;

use tracedecay_domain::errors::{ProjectOpenFailureKind, TraceDecayError};
use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor};
use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor, Result};

use super::rows::audit_read_error;

Expand Down Expand Up @@ -1649,27 +1649,35 @@ pub(super) async fn replace_trigger(
pub(super) async fn trigger_contracts_intact(
conn: &impl QueryExecutor,
) -> tracedecay_domain::errors::Result<bool> {
first_invalid_trigger(conn)
.await
.map(|invalid| invalid.is_none())
.map_err(audit_read_error)
}

pub(in crate::schema_contract) async fn first_invalid_trigger(
conn: &impl QueryExecutor,
) -> Result<Option<&'static Trigger>> {
// SQLite trigger names are unique under ASCII case folding, which is what
// `COLLATE NOCASE` and `to_ascii_lowercase` both apply.
let mut rows = conn
.query(
"SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'trigger'",
(),
)
.await
.map_err(audit_read_error)?;
.await?;
let mut actual = HashMap::new();
while let Some(row) = rows.next().await.map_err(audit_read_error)? {
let name = row.get::<String>(0).map_err(audit_read_error)?;
let table = row.get::<String>(1).map_err(audit_read_error)?;
let sql = row.get::<String>(2).map_err(audit_read_error)?;
while let Some(row) = rows.next().await? {
let name = row.get::<String>(0)?;
let table = row.get::<String>(1)?;
let sql = row.get::<String>(2)?;
actual.insert(name.to_ascii_lowercase(), (table, sql));
}
Ok(INVARIANTS
.iter()
.flat_map(|invariant| invariant.triggers)
.all(|trigger| {
actual
.find(|trigger| {
!actual
.get(&trigger.name.to_ascii_lowercase())
.is_some_and(|(table, sql)| {
table.eq_ignore_ascii_case(trigger.table)
Expand Down
126 changes: 79 additions & 47 deletions crates/tracedecay-global-db/src/schema_contract/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::future::Future;
use std::pin::Pin;
use std::sync::LazyLock;

use tracedecay_runtime_core::db::engine::{QueryExecutor, params};
use tracedecay_runtime_core::db::engine::QueryExecutor;
use tracedecay_runtime_core::db::{DatabaseEngineReadConnection, DatabaseEngineReadSnapshot};
use tracedecay_runtime_core::ports::registered_schema::RegisteredSchemaInstallationV1;
use tracedecay_rusqlite_runtime::repository::{
Expand All @@ -18,7 +18,7 @@ use super::definitions::{
use super::pragma::{
ActualColumn, ActualForeignKey, ActualIndex, ActualTableMetadata, read_table_metadata,
};
use super::{normalize_trigger_sql, starts_with_ignore_ascii_case};
use super::starts_with_ignore_ascii_case;

const OPERATION: &str = "validate global database authority schema";

Expand Down Expand Up @@ -291,45 +291,20 @@ fn validate_indexes_for_table(
Ok(())
}

async fn validate_trigger(
conn: &impl QueryExecutor,
trigger: &super::invariants::Trigger,
) -> tracedecay_domain::errors::Result<()> {
let mut rows = conn
.query(
"SELECT tbl_name, sql FROM sqlite_master
WHERE type = 'trigger' AND name = ?1 COLLATE NOCASE",
params![trigger.name],
)
async fn validate_triggers(conn: &impl QueryExecutor) -> tracedecay_domain::errors::Result<()> {
let invalid = super::invariants::first_invalid_trigger(conn)
.await
.map_err(|error| global_db_operation_error(OPERATION, error))?;
let actual = match rows
.next()
.await
.map_err(|error| global_db_operation_error(OPERATION, error))?
{
Some(row) => Some((
row.get::<String>(0)
.map_err(|error| global_db_operation_error(OPERATION, error))?,
row.get::<String>(1)
.map_err(|error| global_db_operation_error(OPERATION, error))?,
)),
None => None,
};
if actual.as_ref().is_some_and(|(table, sql)| {
table.eq_ignore_ascii_case(trigger.table)
&& normalize_trigger_sql(sql) == normalize_trigger_sql(trigger.create_sql)
}) {
Ok(())
} else {
Err(global_db_operation_message(
if let Some(trigger) = invalid {
return Err(global_db_operation_message(
OPERATION,
format!(
"required trigger '{}' on table '{}' is missing or incompatible",
trigger.name, trigger.table
),
))
));
}
Ok(())
}

async fn validate_observation_autoincrement(
Expand Down Expand Up @@ -559,7 +534,7 @@ pub async fn validate_remote_deletion_schema_contract(
/// schema modules; this validator intentionally neither claims nor validates those domains.
///
/// Each concrete connection implements this instead of one generic async function: nesting
/// `validate_tables_and_indexes` under the trigger walk overflows the next solver's `Send`
/// table and trigger validation overflows the next solver's `Send`
/// proof (rust-lang/rust#159228). The returned future only awaits already-erased children.
pub(crate) trait AuthoritySchemaContract: Sync {
fn validate_authority_schema<'a>(
Expand All @@ -579,18 +554,10 @@ macro_rules! impl_authority_schema_contract {
Box<dyn Future<Output = tracedecay_domain::errors::Result<()>> + Send + '_>,
> = Box::pin(validate_tables_and_indexes(self, TABLES));
tables.await?;
for invariant in super::invariants::INVARIANTS {
for trigger in invariant.triggers {
let checked: Pin<
Box<
dyn Future<Output = tracedecay_domain::errors::Result<()>>
+ Send
+ '_,
>,
> = Box::pin(validate_trigger(self, trigger));
checked.await?;
}
}
let triggers: Pin<
Box<dyn Future<Output = tracedecay_domain::errors::Result<()>> + Send + '_>,
> = Box::pin(validate_triggers(self));
triggers.await?;
let autoincrement: Pin<
Box<dyn Future<Output = tracedecay_domain::errors::Result<()>> + Send + '_>,
> = Box::pin(validate_observation_autoincrement(self));
Expand All @@ -615,9 +582,10 @@ pub(crate) fn validate_authority_schema_contract<'a>(
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};

use super::{normalize_default, validate_tables_and_indexes};
use super::{OPERATION, normalize_default, validate_tables_and_indexes, validate_triggers};
use crate::schema_contract::definitions::TABLES;
use crate::tests::harness::open_registered_test_database_fixture;
use tracedecay_domain::errors::TraceDecayError;
use tracedecay_runtime_core::db::{
TestDatabaseRuntimeScope,
engine::{IntoParams, QueryExecutor, Rows},
Expand Down Expand Up @@ -686,4 +654,68 @@ mod tests {
"column, foreign-key, and complete index metadata each need one query"
);
}

#[asupersync::test]
async fn trigger_contracts_use_one_query_and_preserve_first_failure() {
let directory = tempfile::tempdir().unwrap();
let (database, _owner) = open_registered_test_database_fixture(
&directory.path().join("global.db"),
TestDatabaseRuntimeScope::Profile,
)
.await
.unwrap();
let connection = database.read_connection();
let counted = CountingQueryExecutor::new(&connection);

validate_triggers(&counted).await.unwrap();
assert_eq!(
counted.queries(),
1,
"all trigger definitions share one read"
);

let writer = database.writer_connection().unwrap();
writer
.execute_batch(
"DROP TRIGGER observations_immutable_delete;
DROP TRIGGER observations_immutable_update;
CREATE TRIGGER observations_immutable_update
BEFORE UPDATE ON observations BEGIN SELECT 1; END;",
)
.await
.unwrap();
let error = validate_triggers(&counted).await.unwrap_err();
assert!(
matches!(
&error,
TraceDecayError::Database { operation, message }
if operation == OPERATION
&& message == "required trigger 'observations_immutable_update' on table 'observations' is missing or incompatible"
),
"the first invalid contract must win over inventory order: {error}"
);
assert_eq!(counted.queries(), 2);

writer
.execute_batch(
"DROP TRIGGER observations_immutable_update;
CREATE TRIGGER OBSERVATIONS_IMMUTABLE_UPDATE
BEFORE UPDATE ON observations BEGIN
SELECT RAISE(ABORT, 'observations are immutable');
END;",
)
.await
.unwrap();
let error = validate_triggers(&counted).await.unwrap_err();
assert!(
matches!(
&error,
TraceDecayError::Database { operation, message }
if operation == OPERATION
&& message == "required trigger 'observations_immutable_delete' on table 'observations' is missing or incompatible"
),
"case-folded names must match before the next missing contract is reported: {error}"
);
assert_eq!(counted.queries(), 3);
}
}
12 changes: 1 addition & 11 deletions crates/tracedecay-global-db/src/session_project_rebind_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,17 +349,7 @@ fn lcm_request(provider: &str, session_id: &str) -> LcmCompressionRequest {
stateless_session_patterns: Vec::new(),
ignore_message_patterns: Vec::new(),
expected_current_frontier_store_id: None,
threshold_tokens: None,
max_assembly_tokens: None,
leaf_chunk_tokens: None,
max_source_messages: None,
summary_fan_in: None,
incremental_max_depth: None,
fresh_tail_count: None,
dynamic_leaf_chunk_enabled: None,
dynamic_leaf_chunk_max: None,
context_length: None,
reserve_tokens_floor: None,
overrides: Default::default(),
summarizer: LcmSummarizerMode::Noop,
}
}
Expand Down
Loading
Loading