diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout.rs index ea0e384724..38b1578876 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout.rs @@ -1460,10 +1460,6 @@ impl ContextScoutDurableRuntimeV1 { } } - pub fn is_current(&self, work: ContextScoutWorkV1) -> bool { - self.coalescer.is_current(work) - } - pub(crate) fn restore_startup( &mut self, startup: &ContextScoutDurableStartupOutcomeV1, diff --git a/crates/tracedecay-global-db/src/session_temporal_schema/admission.rs b/crates/tracedecay-global-db/src/session_temporal_schema/admission.rs index 27477c7cde..30a4bc9600 100644 --- a/crates/tracedecay-global-db/src/session_temporal_schema/admission.rs +++ b/crates/tracedecay-global-db/src/session_temporal_schema/admission.rs @@ -1,6 +1,6 @@ use std::collections::BTreeSet; -use sha2::{Digest, Sha256}; +use tracedecay_domain::canonical_text::sha256_hex; use tracedecay_runtime_core::db::engine::{QueryExecutor, params}; use crate::configuration::FreshConfigurationStoreEvidence; @@ -378,7 +378,7 @@ async fn validate_temporal_table_definition_digest( let sql = row .get::(0) .map_err(|error| global_db_operation_error(OPERATION, error))?; - let digest = hex::encode(Sha256::digest(normalize_schema_sql(&sql).as_bytes())); + let digest = sha256_hex(normalize_schema_sql(&sql).as_bytes()); if digest != expected_digest { return Err(session_temporal_reset_required(format!( "temporal table '{table}' has an incompatible CREATE TABLE contract" diff --git a/crates/tracedecay-graph-db/src/generation.rs b/crates/tracedecay-graph-db/src/generation.rs index 15e569ccc8..719edf9149 100644 --- a/crates/tracedecay-graph-db/src/generation.rs +++ b/crates/tracedecay-graph-db/src/generation.rs @@ -9,7 +9,9 @@ use std::sync::{Arc, Condvar, Mutex, OnceLock}; use grafeo_engine::GrafeoDB; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use tracedecay_domain::canonical_text::{encode_lowercase_hex, encode_tagged_lowercase_hex}; +use tracedecay_domain::canonical_text::{ + encode_lowercase_hex, encode_tagged_lowercase_hex, sha256_hex, +}; use tracedecay_store::runtime::{ GraphDependencyGenerationClosureDigestV1, GraphDependencyGenerationIdentityV1, GraphGenerationIdV1, GraphNamespaceV1, GraphProjectionIdV1, GraphProjectionIdentityV1, @@ -885,10 +887,7 @@ pub(crate) fn physical_namespace( "failed to encode physical graph generation identity: {error}" )) })?; - GraphNamespace::new(format!( - "generation:{}", - hex::encode(Sha256::digest(encoded)) - )) + GraphNamespace::new(format!("generation:{}", sha256_hex(&encoded))) } pub(crate) fn is_physical_generation_namespace(namespace: &GraphNamespace) -> bool { diff --git a/crates/tracedecay-graph-db/src/store_quarantine.rs b/crates/tracedecay-graph-db/src/store_quarantine.rs index 4bfad6e315..73b03b3a70 100644 --- a/crates/tracedecay-graph-db/src/store_quarantine.rs +++ b/crates/tracedecay-graph-db/src/store_quarantine.rs @@ -42,7 +42,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use fs2::FileExt; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; +use tracedecay_domain::canonical_text::sha256_hex; use tracedecay_private_fs::framed_log::{DirectorySyncPolicy, atomic_write, sync_directory}; use crate::{GraphDb, GraphDbError}; @@ -347,7 +347,7 @@ fn quarantine_durability_failure( } fn fault_fingerprint(fault: &str) -> String { - format!("sha256:{}", hex::encode(Sha256::digest(fault.as_bytes()))) + format!("sha256:{}", sha256_hex(fault.as_bytes())) } /// `graph.grafeo` -> `graph.grafeo.wal`, matching Grafeo's sidecar layout. diff --git a/crates/tracedecay-graph-query/src/context/read_modes.rs b/crates/tracedecay-graph-query/src/context/read_modes.rs index eadb82fdf3..1ecdef85bc 100644 --- a/crates/tracedecay-graph-query/src/context/read_modes.rs +++ b/crates/tracedecay-graph-query/src/context/read_modes.rs @@ -64,14 +64,22 @@ pub fn estimate_tokens(s: &str) -> u32 { s.chars().count().div_ceil(4).min(u32::MAX as usize) as u32 } pub fn render_lines(source: &str, range: LineRange) -> String { - let lines = source.lines().collect::>(); let start = range.start.saturating_sub(1) as usize; - let end = (range.end as usize).min(lines.len()); - if start >= end { - String::new() - } else { - lines[start..end].join("\n") + let end_inclusive = range.end as usize; + if start >= end_inclusive { + return String::new(); } + let mut selected = source.lines().skip(start).take(end_inclusive - start); + let Some(first) = selected.next() else { + return String::new(); + }; + let mut body = String::with_capacity(first.len()); + body.push_str(first); + for line in selected { + body.push('\n'); + body.push_str(line); + } + body } #[hotpath::measure(label = "usecases.context.render_map")] diff --git a/crates/tracedecay-mcp/src/handlers/analysis/field_sites.rs b/crates/tracedecay-mcp/src/handlers/analysis/field_sites.rs index 65a677868b..85ad8c5fc2 100644 --- a/crates/tracedecay-mcp/src/handlers/analysis/field_sites.rs +++ b/crates/tracedecay-mcp/src/handlers/analysis/field_sites.rs @@ -67,15 +67,14 @@ pub async fn handle_field_sites( // nodes anyway cost one daemon round trip per file in the project — // O(store) work to answer a question whose result is a handful of // sites. - let masked = if path_is_rust(file) { + let masked = path_is_rust(file).then(|| { tracedecay_code_extraction::source_mask::masked_rust_source_with( &source, tracedecay_code_extraction::source_mask::MaskOptions::CODE_SCAN, ) - } else { - source.clone() - }; - let sites = find_field_references(&masked, &field_name); + }); + let sites = + find_field_references(masked.as_deref().unwrap_or(&source), &field_name); if sites.is_empty() { continue; } diff --git a/crates/tracedecay-mcp/src/handlers/analysis/unsafe_patterns.rs b/crates/tracedecay-mcp/src/handlers/analysis/unsafe_patterns.rs index 5f75084623..8dc3253368 100644 --- a/crates/tracedecay-mcp/src/handlers/analysis/unsafe_patterns.rs +++ b/crates/tracedecay-mcp/src/handlers/analysis/unsafe_patterns.rs @@ -194,14 +194,13 @@ pub async fn handle_unsafe_patterns( // masked copy; the original line is kept for the emitted snippet. // Non-Rust files are scanned raw (the Rust grammar would // mis-tokenise them). - let masked = if path_is_rust(file) { + let masked_owned = path_is_rust(file).then(|| { tracedecay_code_extraction::source_mask::masked_rust_source_with( &source, tracedecay_code_extraction::source_mask::MaskOptions::CODE_SCAN, ) - } else { - source.clone() - }; + }); + let masked = masked_owned.as_deref().unwrap_or(&source); let test_lines = if path_is_rust(file) { tracedecay_code_extraction::source_mask::rust_test_lines(&source).map_err( |error| { diff --git a/crates/tracedecay-mcp/src/handlers/graph/navigation.rs b/crates/tracedecay-mcp/src/handlers/graph/navigation.rs index 070ad67047..a66fbdf8f9 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/navigation.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/navigation.rs @@ -799,11 +799,17 @@ fn collect_method_bodies( }); let mut out: Vec = Vec::new(); + let mut cached_path: Option = None; + let mut cached_source = String::new(); for (file_path, _, child) in methods { let metadata = required_graph_metadata(&child)?; - let source = graph.read_indexed_source_file(&file_path)?; + if cached_path.as_deref() != Some(file_path.as_str()) { + cached_source = graph.read_indexed_source_file(&file_path)?; + cached_path = Some(file_path); + } let end_line = graph_symbol_end_line(metadata)?; - let body = crate::handlers::info::extract_lines(&source, metadata.start_line, end_line); + let body = + crate::handlers::info::extract_lines(&cached_source, metadata.start_line, end_line); out.push(json!({ "name": metadata.simple_name, "kind": metadata.kind, diff --git a/crates/tracedecay-mcp/src/handlers/info/body.rs b/crates/tracedecay-mcp/src/handlers/info/body.rs index 4e0503b68f..8da2c82a27 100644 --- a/crates/tracedecay-mcp/src/handlers/info/body.rs +++ b/crates/tracedecay-mcp/src/handlers/info/body.rs @@ -19,13 +19,22 @@ use super::verified::{ /// raw tree-sitter row index, so the caller passes them through unchanged. /// Returns the empty string if the range is out of bounds. pub fn extract_lines(source: &str, start_line: u32, end_line: u32) -> String { - let lines: Vec<&str> = source.lines().collect(); let start = start_line as usize; - let end = (end_line as usize).saturating_add(1).min(lines.len()); - if start >= lines.len() || start >= end { + let end_exclusive = (end_line as usize).saturating_add(1); + if start >= end_exclusive { return String::new(); } - lines[start..end].join("\n") + let mut selected = source.lines().skip(start).take(end_exclusive - start); + let Some(first) = selected.next() else { + return String::new(); + }; + let mut body = String::with_capacity(first.len()); + body.push_str(first); + for line in selected { + body.push('\n'); + body.push_str(line); + } + body } #[hotpath::measure(label = "mcp.info.body.total")] diff --git a/crates/tracedecay-session-memory/src/fact_store/graph_manifest.rs b/crates/tracedecay-session-memory/src/fact_store/graph_manifest.rs index bb9b433507..f38c9b09ff 100644 --- a/crates/tracedecay-session-memory/src/fact_store/graph_manifest.rs +++ b/crates/tracedecay-session-memory/src/fact_store/graph_manifest.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use sha2::{Digest, Sha256}; use tracedecay_domain::FactOwnerV1; +use tracedecay_domain::canonical_text::sha256_hex; use tracedecay_graph_db::{ GraphEntity, GraphEntityId, GraphEntityRef, GraphGenerationId, GraphGenerationManifest, GraphGenerationRelation, GraphLabel, GraphProjectionIdentity, GraphRelationId, @@ -53,13 +54,13 @@ pub(super) fn build_manifest( .map_err(|error| graph_error(owner, error))?; insert_projection_entity(&mut entity_ids, from.clone())?; insert_projection_entity(&mut entity_ids, to.clone())?; - let relation_digest = hex::encode(Sha256::digest( + let relation_digest = sha256_hex( format!( "{}\0{}\0{}", relation.source, relation.target, relation.kind ) .as_bytes(), - )); + ); relations.push( GraphGenerationRelation::new( GraphRelationId::new(format!("memory-relation:{relation_digest}")) diff --git a/crates/tracedecay-session-temporal-store/src/relations.rs b/crates/tracedecay-session-temporal-store/src/relations.rs index dfde9dd1fa..0a4100905f 100644 --- a/crates/tracedecay-session-temporal-store/src/relations.rs +++ b/crates/tracedecay-session-temporal-store/src/relations.rs @@ -8,8 +8,8 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::sync::Arc; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use thiserror::Error; +use tracedecay_domain::canonical_text::sha256_hex; use tracedecay_domain::{ AgentInstanceId, CopyProofV1, MessageOccurrenceIdV1, ProjectId, RetrievalAnchorId, SessionId, TemporalValidityV1, ThreadId, UserProfileId, UtcMicros, @@ -405,11 +405,8 @@ pub(crate) fn projection_watermark( .then_with(|| left.agent_label.cmp(&right.agent_label)) }); let encoded = serde_json::to_vec(&canonical).map_err(|_| SessionRelationError::Invalid)?; - GraphWatermark::new(format!( - "session-relations:{}", - hex::encode(Sha256::digest(encoded)) - )) - .map_err(map_graph_error) + GraphWatermark::new(format!("session-relations:{}", sha256_hex(&encoded))) + .map_err(map_graph_error) } fn build_graph( diff --git a/crates/tracedecay-sessions/src/runtime/git_correlation/attribution.rs b/crates/tracedecay-sessions/src/runtime/git_correlation/attribution.rs index e0f3cad27b..41b67e00e0 100644 --- a/crates/tracedecay-sessions/src/runtime/git_correlation/attribution.rs +++ b/crates/tracedecay-sessions/src/runtime/git_correlation/attribution.rs @@ -1,14 +1,13 @@ use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; -use sha2::{Digest as _, Sha256}; use tracedecay_graph_db::{GraphIdempotencyKey, GraphNamespace, GraphProjectorRevision}; use tracedecay_runtime_core::shard_runtime::VerifiedGraphRuntimePortV1; use super::{ CommitEvidence, CommitRelation, CommitSessionRecord, GIT_EVIDENCE_PROJECTOR_REVISION, GitCorrelationError, GitCorrelationSessionStore, GitEvidenceProjectionStore, - GitEvidenceProjectionV1, SessionGitSpan, SpanObservation, SpanOverlapKind, + GitEvidenceProjectionV1, SessionGitSpan, SpanObservation, SpanOverlapKind, digest_bytes, git_evidence_projection_identity, normalize_worktree, observation_extends_span, providers_compatible, publish_git_evidence_projection, recover_git_evidence_projection, }; @@ -185,10 +184,7 @@ pub fn stable_backfill_span( branch.unwrap_or("\0") ); SessionGitSpan { - span_id: format!( - "backfill:{}", - hex::encode(Sha256::digest(identity.as_bytes())) - ), + span_id: format!("backfill:{}", digest_bytes(identity.as_bytes())), provider: provider.to_owned(), session_id: session_id.to_owned(), thread_id: None, @@ -483,10 +479,7 @@ fn transcript_span_id(observation: &SpanObservation, worktree: &str) -> String { "{}\0{}\0{thread_id}\0{branch}\0{}\0{}\0{:?}", observation.provider, observation.session_id, worktree, observation.ts, observation.source, ); - format!( - "transcript:{}", - hex::encode(Sha256::digest(material.as_bytes())) - ) + format!("transcript:{}", digest_bytes(material.as_bytes())) } pub fn graph_evidence_publication_key( @@ -495,7 +488,7 @@ pub fn graph_evidence_publication_key( commits: &[CommitSessionRecord], ) -> Result { let bytes = serde_json::to_vec(&(prefix, spans, commits))?; - Ok(format!("{prefix}:{}", hex::encode(Sha256::digest(bytes)))) + Ok(format!("{prefix}:{}", digest_bytes(&bytes))) } fn merge_span(spans: &mut Vec, incoming: &SessionGitSpan) -> bool { diff --git a/crates/tracedecay-sessions/src/runtime/git_correlation/publication_outbox.rs b/crates/tracedecay-sessions/src/runtime/git_correlation/publication_outbox.rs index 7563536029..7af6aae340 100644 --- a/crates/tracedecay-sessions/src/runtime/git_correlation/publication_outbox.rs +++ b/crates/tracedecay-sessions/src/runtime/git_correlation/publication_outbox.rs @@ -1,10 +1,9 @@ use serde::{Deserialize, Serialize}; -use sha2::{Digest as _, Sha256}; use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor, params}; use super::{ CommitSessionRecord, GitCorrelationError, GitCorrelationSessionStore, GitCorrelationWriteTxn, - SpanObservation, + SpanObservation, digest_bytes, }; /// Maximum exact receipts replayed by one startup or host-admission pass. @@ -62,7 +61,7 @@ pub async fn enqueue_git_evidence_publication( let receipt_material = serde_json::to_vec(&(publication_prefix, &payload))?; let receipt_id = format!( "git-evidence-publication:{}", - hex::encode(Sha256::digest(receipt_material)) + digest_bytes(&receipt_material) ); conn.execute( "INSERT OR IGNORE INTO git_evidence_publication_outbox ( diff --git a/crates/tracedecay-sessions/src/runtime/git_correlation/store.rs b/crates/tracedecay-sessions/src/runtime/git_correlation/store.rs index 706215e087..faa1fa5637 100644 --- a/crates/tracedecay-sessions/src/runtime/git_correlation/store.rs +++ b/crates/tracedecay-sessions/src/runtime/git_correlation/store.rs @@ -5,7 +5,6 @@ use std::future::Future; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use sha2::{Digest as _, Sha256}; use tracedecay_graph_db::{ GraphCancellation, GraphDbError, GraphEntity, GraphEntityId, GraphEntityRef, GraphGenerationId, GraphGenerationManifest, GraphGenerationRelation, GraphIdempotencyKey, GraphLabel, @@ -25,8 +24,8 @@ use super::{ GitCorrelationError, GitEvidenceProjectionV1, GitRefFilter, GitScopeFilter, SessionGitCorrelationHit, SessionGitSpan, SessionsForQuery, SpanObservation, canonical_provider_map, commit_hits, commit_identities_with_producer_fallback, - commit_record_matches_query, commit_record_order, scope_session_ids, sessions_for_limit, - span_hits, span_matches_query, + commit_record_matches_query, commit_record_order, digest_bytes, scope_session_ids, + sessions_for_limit, span_hits, span_matches_query, }; const GRAPH_READ_PAGE_ITEMS: usize = 10_000; @@ -102,11 +101,8 @@ pub fn git_evidence_generation_id( projection, projector_revision, ))?; - GraphGenerationId::new(format!( - "session-git-evidence:{}", - hex::encode(Sha256::digest(bytes)) - )) - .map_err(Into::into) + GraphGenerationId::new(format!("session-git-evidence:{}", digest_bytes(&bytes))) + .map_err(Into::into) } /// Projects the complete evidence into one generation manifest. @@ -1452,7 +1448,7 @@ fn stable_identity(kind: &str, material: &str) -> String { } fn stable_digest(material: &str) -> String { - hex::encode(Sha256::digest(material.as_bytes())) + digest_bytes(material.as_bytes()) } struct AtomicGraphCancellation(Arc); diff --git a/crates/tracedecay-store-runtime/src/remote_query.rs b/crates/tracedecay-store-runtime/src/remote_query.rs index dd5d49a597..a3677e33b9 100644 --- a/crates/tracedecay-store-runtime/src/remote_query.rs +++ b/crates/tracedecay-store-runtime/src/remote_query.rs @@ -27,6 +27,7 @@ use tracedecay_contracts::{ EvidenceDomain, EvidencePacket, OperationBudgetUsage, OperationReceipt, PageState, RetrievalEvidence, TemporalState, }; +use tracedecay_domain::canonical_text::sha256_hex; use tracedecay_domain::{CurrentRemoteAuthorityStateV1, UtcMicros}; use tracedecay_rusqlite_runtime::remote::{RemoteQueryAuthoritySnapshotV1, RemoteSqliteStorageV1}; use tracedecay_store::{ @@ -355,11 +356,14 @@ fn current_frontier( fn runtime_control( command: &RemoteExactObservationQueryCommandV1, ) -> Result { - let suffix = hex::encode(Sha256::digest(format!( - "{}:{}", - command.request_id.as_str(), - command.observation_id.as_str() - ))); + let suffix = sha256_hex( + format!( + "{}:{}", + command.request_id.as_str(), + command.observation_id.as_str() + ) + .as_bytes(), + ); Ok(RuntimeRequestControlV1 { requested_at: command.observed_at, deadline: RuntimeDeadlineV1 { diff --git a/dashboard/src/workspaces/code/IndexFreshness.tsx b/dashboard/src/workspaces/code/IndexFreshness.tsx index 10744bf4f1..28a31c3753 100644 --- a/dashboard/src/workspaces/code/IndexFreshness.tsx +++ b/dashboard/src/workspaces/code/IndexFreshness.tsx @@ -35,7 +35,15 @@ import { fetchEnvelope, type EnvelopeResult } from '../../data/query/envelope.ts import { scopeKey, scopedUrl, useScope } from '../../data/scope/store.ts'; import { authorizationState } from '../../ui/EnvelopeTruth.tsx'; import { StateChip } from '../../ui/StateChip.tsx'; -import { elideStart, formatCount, formatMicrosUtc, splitBytes } from '../../ui/format.ts'; +import { elideStart, formatCount, formatMicrosUtc } from '../../ui/format.ts'; +import { + codeIndexBlockedReasonLabel, + codeIndexPhaseLabel, + codeIndexProgressPercentage, + formatDurationMicros, + isCurrentOrNewerCodeIndexProgress, +} from '../observatory/CodeIndexPipeline.tsx'; +import { formatBytes } from '../observatory/storageModel.ts'; type CodeIndexBuildProgress = NonNullable; @@ -203,7 +211,7 @@ function BuildProgressReading({ progress: CodeIndexBuildProgress; observedAtMicros: number; }) { - const percentage = progressPercentage(progress); + const percentage = codeIndexProgressPercentage(progress); const quietSeconds = progress.phase !== 'ready' && observedAtMicros - progress.last_progress_micros >= STALLED_AFTER_SECONDS * 1_000_000 @@ -219,7 +227,7 @@ function BuildProgressReading({
Code progress - {phaseLabel(progress.phase)} · {percentage.toFixed(1)}% + {codeIndexPhaseLabel(progress.phase)} · {percentage.toFixed(1)}%
{progress.blocked_reason ? ( -

blocked: {blockedReasonLabel(progress.blocked_reason)}

+

blocked: {codeIndexBlockedReasonLabel(progress.blocked_reason)}

) : null} ); @@ -337,7 +345,7 @@ function useLatestBuildProgress( next.delete(worktree.worktree_root); changed = true; } - } else if (!current || isCurrentOrNewerProgress(incoming, current)) { + } else if (!current || isCurrentOrNewerCodeIndexProgress(incoming, current)) { next.set(worktree.worktree_root, incoming); changed = true; } @@ -348,66 +356,6 @@ function useLatestBuildProgress( return latestProgress; } -function isCurrentOrNewerProgress( - incoming: CodeIndexBuildProgress, - rendered: CodeIndexBuildProgress, -): boolean { - if (incoming.daemon_incarnation !== rendered.daemon_incarnation) { - return incoming.daemon_incarnation > rendered.daemon_incarnation; - } - if (incoming.producer_incarnation !== rendered.producer_incarnation) { - return incoming.producer_incarnation > rendered.producer_incarnation; - } - return incoming.progress_epoch >= rendered.progress_epoch; -} - -function progressPercentage(progress: CodeIndexBuildProgress): number { - const completed = - progress.total_lexical_units > 0 - ? progress.completed_lexical_units / progress.total_lexical_units - : progress.phase === 'ready' - ? 1 - : 0; - return Math.min(100, Math.max(0, completed * 100)); -} - -function phaseLabel(phase: CodeIndexBuildProgress['phase']): string { - switch (phase) { - case 'source_scan': - return 'source scan'; - case 'relational_preparation': - return 'relational preparation'; - case 'bulk_commit': - return 'bulk commit'; - case 'index_build': - return 'index build'; - case 'verification': - return 'verification'; - case 'ready': - return 'ready'; - } -} - -function blockedReasonLabel(reason: NonNullable): string { - switch (reason) { - case 'resident_memory': - return 'resident memory'; - case 'source_unavailable': - return 'source unavailable'; - case 'artifact_store_unavailable': - return 'artifact store unavailable'; - case 'retry_backoff': - return 'retry backoff'; - case 'publication_authority_corrupt': - return 'publication authority corrupt'; - } -} - -function formatBytes(bytes: number): string { - const { value, unit } = splitBytes(bytes); - return unit ? `${value} ${unit}` : value; -} - function formatDurationSeconds(seconds: number): string { if (seconds < 90) return `${Math.round(seconds)}s`; if (seconds < 5_400) return `${Math.round(seconds / 60)}m`; @@ -416,12 +364,6 @@ function formatDurationSeconds(seconds: number): string { return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; } -function formatDurationMicros(micros: number): string { - if (micros < 1_000) return `${micros}µs`; - if (micros < 1_000_000) return `${Math.round(micros / 1_000)}ms`; - return formatDurationSeconds(micros / 1_000_000); -} - function Row({ label, children, diff --git a/dashboard/src/workspaces/knowledge/FactTrustHistory.tsx b/dashboard/src/workspaces/knowledge/FactTrustHistory.tsx index 4b380bdf0b..ec95e005ec 100644 --- a/dashboard/src/workspaces/knowledge/FactTrustHistory.tsx +++ b/dashboard/src/workspaces/knowledge/FactTrustHistory.tsx @@ -24,11 +24,12 @@ import type { EChartsOption } from 'echarts'; import { PayloadBoundary } from '../../ui/ReadSection.tsx'; import { StateChip } from '../../ui/StateChip.tsx'; +import { formatMicrosUtc } from '../../ui/format.ts'; import { Readout } from '../../ui/instrument.tsx'; import { Chart } from '../../viz/chart/Chart.tsx'; import type { PayloadResult } from '../../data/query/payload.ts'; import type { TrustHistoryEvent, TrustHistoryPayload } from '../../data/query/memory.ts'; -import { formatUtcMicros, trustDetailState, trustHistoryReading } from './memoryModel.ts'; +import { trustDetailState, trustHistoryReading } from './memoryModel.ts'; export function TrustHistorySection({ pending, @@ -159,7 +160,7 @@ function TrustTrace({ data }: { data: TrustHistoryPayload }) { () => ({ xAxis: { type: 'category', - data: events.map((event) => formatUtcMicros(event.timestamp)), + data: events.map((event) => formatMicrosUtc(event.timestamp)), axisLabel: { show: false }, axisTick: { show: false }, }, @@ -183,17 +184,17 @@ function TrustTrace({ data }: { data: TrustHistoryPayload }) { return (
- {formatUtcMicros(first.timestamp).slice(0, 10)} + {formatMicrosUtc(first.timestamp).slice(0, 10)} trust after each event · 0 to 1 - {formatUtcMicros(last.timestamp).slice(0, 10)} + {formatMicrosUtc(last.timestamp).slice(0, 10)}
@@ -207,7 +208,7 @@ function TrustEventRow({ event }: { event: TrustHistoryEvent }) {
  • - {formatUtcMicros(event.timestamp)} + {formatMicrosUtc(event.timestamp)} {event.action} diff --git a/dashboard/src/workspaces/knowledge/MemoryOplog.tsx b/dashboard/src/workspaces/knowledge/MemoryOplog.tsx index 29a0e5e842..4fccf87868 100644 --- a/dashboard/src/workspaces/knowledge/MemoryOplog.tsx +++ b/dashboard/src/workspaces/knowledge/MemoryOplog.tsx @@ -10,9 +10,10 @@ * detail. */ import { PayloadBoundary } from '../../ui/ReadSection.tsx'; +import { formatMicrosUtc } from '../../ui/format.ts'; import { Panel, Readout } from '../../ui/instrument.tsx'; import { useMemoryOplog, type OplogEvent, type OplogPayload } from '../../data/query/memory.ts'; -import { formatUtcMicros, oplogReading } from './memoryModel.ts'; +import { oplogReading } from './memoryModel.ts'; export function MemoryOplog() { const oplog = useMemoryOplog(); @@ -97,7 +98,7 @@ function OplogRow({ event }: { event: OplogEvent }) {

  • - {formatUtcMicros(event.ts)} + {formatMicrosUtc(event.ts)} {event.op} {/* `fact_id` is null only for an operation with no canonical fact target. */} diff --git a/dashboard/src/workspaces/knowledge/memoryModel.ts b/dashboard/src/workspaces/knowledge/memoryModel.ts index 1b470ce650..d6cc9255f7 100644 --- a/dashboard/src/workspaces/knowledge/memoryModel.ts +++ b/dashboard/src/workspaces/knowledge/memoryModel.ts @@ -30,11 +30,6 @@ import type { /* ---- trust history ------------------------------------------------------- */ -/** Formats canonical UTC microseconds only at the presentation boundary. */ -export function formatUtcMicros(value: number): string { - return new Date(Math.trunc(value / 1_000)).toISOString(); -} - export interface TrustHistoryReading { /** Events this bounded audit response returned. */ readonly count: number; diff --git a/dashboard/src/workspaces/observatory/CodeIndexPipeline.tsx b/dashboard/src/workspaces/observatory/CodeIndexPipeline.tsx index fbde56a10c..0de08be54c 100644 --- a/dashboard/src/workspaces/observatory/CodeIndexPipeline.tsx +++ b/dashboard/src/workspaces/observatory/CodeIndexPipeline.tsx @@ -251,7 +251,7 @@ function sameCodeIndexProgressMap( return true; } -function isCurrentOrNewerCodeIndexProgress( +export function isCurrentOrNewerCodeIndexProgress( incoming: CodeIndexBuildProgressV1, rendered: CodeIndexBuildProgressV1, ): boolean { @@ -291,7 +291,9 @@ export function codeIndexPhaseLabel(phase: CodeIndexBuildProgressV1['phase']): s } } -function codeIndexBlockedReasonLabel(reason: CodeIndexBuildProgressV1['blocked_reason']): string { +export function codeIndexBlockedReasonLabel( + reason: CodeIndexBuildProgressV1['blocked_reason'], +): string { switch (reason) { case 'resident_memory': return 'resident memory'; diff --git a/dashboard/src/workspaces/observatory/EvidenceSummaries.tsx b/dashboard/src/workspaces/observatory/EvidenceSummaries.tsx index 8641c006a9..b511d4c7fb 100644 --- a/dashboard/src/workspaces/observatory/EvidenceSummaries.tsx +++ b/dashboard/src/workspaces/observatory/EvidenceSummaries.tsx @@ -12,7 +12,7 @@ import type { StorageFindingsPayloadV1, StorageTelemetryPayloadV1, } from '../../contracts/generated.ts'; -import type { EnvelopeResult } from '../../data/query/envelope.ts'; +import { envelopePayload } from '../../data/query/useEnvelope.ts'; import { cn } from '../../ui/cn.ts'; import { MeterRow } from '../../ui/instrument.tsx'; import { humanizeMetric } from '../../ui/metricModel.ts'; @@ -53,10 +53,6 @@ import { formatBytes, storageFindingLabel } from './storageModel.ts'; * payload is the daemon's word, not a skeleton that looks like data. */ -function envelopePayload(result: EnvelopeResult | undefined): T | null { - return result?.outcome === 'envelope' ? result.envelope.payload : null; -} - function Rows({ children, label }: { children: ReactNode; label: string }) { return (