From 7ad21f2c390742dd282a102fe6f2b4e63b2fa0fd Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:11:48 -0700 Subject: [PATCH 1/6] fix(dashboard): generate settings PATCH contracts --- .../workflows/dashboard-contract-export.yml | 73 +++++ crates/tracedecay-api/src/configuration.rs | 111 +++++-- .../src/result/envelope.rs | 2 +- .../src/result/evidence.rs | 33 ++- .../src/result/receipt.rs | 2 +- .../src/settings_preview.rs | 3 +- .../src/application_surface.rs | 11 +- .../src/automation_config_api.rs | 36 +-- .../src/contract_schema.rs | 6 +- .../src/settings_api.rs | 88 +++--- .../src/settings_api/tests/wire.rs | 272 ++++++++++++++++++ dashboard/codegen/src/generate.ts | 29 +- dashboard/codegen/tests/generate.test.ts | 45 ++- 13 files changed, 594 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/dashboard-contract-export.yml create mode 100644 crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs diff --git a/.github/workflows/dashboard-contract-export.yml b/.github/workflows/dashboard-contract-export.yml new file mode 100644 index 0000000000..a4eba1eb74 --- /dev/null +++ b/.github/workflows/dashboard-contract-export.yml @@ -0,0 +1,73 @@ +# Temporary build route: remove after importing the canonical generated JSON. +name: Export dashboard contract artifacts +run-name: Export dashboard contracts for ${{ github.event.pull_request.head.sha }} + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: dashboard-contract-export-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + export: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 90 + env: + CARGO_BAZEL_TIMEOUT: "3600" + EXPORT_DIRECTORY: ${{ runner.temp }}/dashboard-contract-export + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: "26" + + - uses: ./.github/actions/setup-pnpm + + - uses: ./.github/actions/setup-bazel + with: + cache-key: ci-linux-test-v2 + + - name: Export the canonical dashboard schema + run: | + set -euo pipefail + mkdir -p "$EXPORT_DIRECTORY" + bazel run --config=ci --jobs=1 //sdks/codegen:dashboard_schema -- "$EXPORT_DIRECTORY/dashboard-contracts.schema.json" + test -s "$EXPORT_DIRECTORY/dashboard-contracts.schema.json" + + # Preserve the schema before the independent fixture build starts. + - name: Upload the canonical dashboard schema + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: dashboard-schema-${{ github.event.pull_request.head.sha }} + path: ${{ runner.temp }}/dashboard-contract-export/dashboard-contracts.schema.json + if-no-files-found: error + retention-days: 1 + + - name: Serialize the real settings response fixture + run: | + set -euo pipefail + bazel test --config=ci --jobs=1 //crates/tracedecay-dashboard-api:unit_test \ + --test_arg=settings_api::tests::wire::project_patch_serialization_preserves_settlement_and_refusals \ + --test_arg=--exact --test_output=all --cache_test_results=no \ + --nozip_undeclared_test_outputs + fixture=bazel-testlogs/crates/tracedecay-dashboard-api/unit_test/test.outputs/settings-patch.json + test -s "$fixture" + cp -- "$fixture" "$EXPORT_DIRECTORY/settings-patch.json" + + - name: Upload the canonical settings response fixture + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: settings-patch-fixture-${{ github.event.pull_request.head.sha }} + path: ${{ runner.temp }}/dashboard-contract-export/settings-patch.json + if-no-files-found: error + retention-days: 1 diff --git a/crates/tracedecay-api/src/configuration.rs b/crates/tracedecay-api/src/configuration.rs index 4817efb7a1..6443e3dd33 100644 --- a/crates/tracedecay-api/src/configuration.rs +++ b/crates/tracedecay-api/src/configuration.rs @@ -9,8 +9,10 @@ use axum::Json; use axum::http::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; -use tracedecay_contracts::{ApplicationProblemEnvelope, ApplicationProblemKind}; +use serde_json::Value; +use tracedecay_contracts::{ + ApplicationProblemEnvelope, ApplicationProblemKind, SettingsValidationIssueV1, +}; use tracedecay_domain::configuration::{CodeIndexWorkerSelectionV1, ContextScoutIntentSettingsV1}; use tracedecay_tool_catalog::ApplicationSurfaceOperation; @@ -89,8 +91,39 @@ pub struct CodeIndexWorkerSettingsPatch { pub code_index_workers: CodeIndexWorkerSelectionV1, } +/// Configuration refusals carrying the same stable codes as the HTTP routes. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "code", rename_all = "snake_case", deny_unknown_fields)] +pub enum ConfigurationRefusalV1 { + ConfigurationRevisionConflict { + detail: String, + expected_revision_id: String, + actual_revision_id: String, + }, + ConfigurationAuthorityUnavailable { + detail: String, + }, + ApplicationContractViolation { + detail: String, + }, +} + +/// Every refusal body produced by the dashboard configuration adapter. The +/// untagged wrapper preserves the existing validation, CAS, and application +/// problem wire formats while giving producers and consumers one authority. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +pub enum DashboardConfigurationRefusalV1 { + Validation { + detail: String, + validation_errors: Vec, + }, + Configuration(ConfigurationRefusalV1), + ApplicationProblem(ApplicationProblemEnvelope), +} + /// Axum-compatible typed error used by dashboard configuration handlers. -pub type DashboardConfigurationRouteErrorV1 = (StatusCode, Json); +pub type DashboardConfigurationRouteErrorV1 = (StatusCode, Json); /// Parse a project settings patch while preserving the dashboard's established /// malformed-payload response shape. @@ -175,13 +208,15 @@ pub fn validate_code_index_worker_settings_patch( } /// Render validation failures using the generated dashboard wire shape. -pub fn settings_validation_error(errors: impl Serialize) -> DashboardConfigurationRouteErrorV1 { +pub fn settings_validation_error( + errors: impl IntoIterator, +) -> DashboardConfigurationRouteErrorV1 { ( StatusCode::BAD_REQUEST, - Json(json!({ - "detail": "settings validation failed", - "validation_errors": errors, - })), + Json(DashboardConfigurationRefusalV1::Validation { + detail: "settings validation failed".to_owned(), + validation_errors: errors.into_iter().collect(), + }), ) } @@ -193,12 +228,13 @@ pub fn configuration_revision_conflict_error( ) -> DashboardConfigurationRouteErrorV1 { ( StatusCode::CONFLICT, - Json(json!({ - "code": "configuration_revision_conflict", - "detail": detail, - "expected_revision_id": expected, - "actual_revision_id": actual, - })), + Json(DashboardConfigurationRefusalV1::Configuration( + ConfigurationRefusalV1::ConfigurationRevisionConflict { + detail: detail.to_owned(), + expected_revision_id: expected.to_owned(), + actual_revision_id: actual.to_owned(), + }, + )), ) } @@ -206,10 +242,11 @@ pub fn configuration_revision_conflict_error( pub fn configuration_authority_unavailable_error() -> DashboardConfigurationRouteErrorV1 { ( StatusCode::SERVICE_UNAVAILABLE, - Json(json!({ - "code": "configuration_authority_unavailable", - "detail": "configuration authority is unavailable", - })), + Json(DashboardConfigurationRefusalV1::Configuration( + ConfigurationRefusalV1::ConfigurationAuthorityUnavailable { + detail: "configuration authority is unavailable".to_owned(), + }, + )), ) } @@ -225,13 +262,17 @@ pub fn configuration_application_problem_error( } else { crate::application_problem_status(problem.problem.kind) }; - let payload = serde_json::to_value(problem) - .unwrap_or_else(|_| json!({ "detail": "configuration mutation was rejected" })); - (status, Json(payload)) + ( + status, + Json(DashboardConfigurationRefusalV1::ApplicationProblem(problem)), + ) } -fn validation_error(field: &str, message: &str) -> Value { - json!({ "field": field, "message": message }) +fn validation_error(field: &str, message: &str) -> SettingsValidationIssueV1 { + SettingsValidationIssueV1 { + field: field.to_owned(), + message: message.to_owned(), + } } fn patch_shape_error(scope: &str, error: &serde_json::Error) -> DashboardConfigurationRouteErrorV1 { @@ -239,10 +280,10 @@ fn patch_shape_error(scope: &str, error: &serde_json::Error) -> DashboardConfigu let field = serde_error_field(&message).unwrap_or_else(|| "patch".to_owned()); ( StatusCode::BAD_REQUEST, - Json(json!({ - "detail": message, - "validation_errors": [{ "field": field, "message": message }], - })), + Json(DashboardConfigurationRefusalV1::Validation { + detail: message.clone(), + validation_errors: vec![SettingsValidationIssueV1 { field, message }], + }), ) } @@ -260,6 +301,7 @@ fn serde_error_field(message: &str) -> Option { #[cfg(test)] mod tests { use super::*; + use serde_json::json; #[test] fn user_worker_patch_round_trips_the_tagged_contract() { @@ -298,7 +340,10 @@ mod tests { let (status, Json(body)) = validate_code_index_worker_settings_patch(&patch).expect_err("zero must be denied"); assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(body["validation_errors"][0]["field"], "code_index_workers"); + assert_eq!( + serde_json::to_value(body).unwrap()["validation_errors"][0]["field"], + "code_index_workers" + ); } #[test] @@ -324,7 +369,10 @@ mod tests { let (status, Json(body)) = admit_user_settings_patch(&invalid, |_| None) .expect_err("zero duration must be denied"); assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(body["validation_errors"][0]["field"], "watcher_debounce"); + assert_eq!( + serde_json::to_value(body).unwrap()["validation_errors"][0]["field"], + "watcher_debounce" + ); } #[test] @@ -338,6 +386,9 @@ mod tests { let (status, Json(body)) = result.expect_err("mixed authority must be rejected"); assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(body["validation_errors"][0]["field"], "code_index_workers"); + assert_eq!( + serde_json::to_value(body).unwrap()["validation_errors"][0]["field"], + "code_index_workers" + ); } } diff --git a/crates/tracedecay-contracts/src/result/envelope.rs b/crates/tracedecay-contracts/src/result/envelope.rs index 28c5b3b297..4a528b6a36 100644 --- a/crates/tracedecay-contracts/src/result/envelope.rs +++ b/crates/tracedecay-contracts/src/result/envelope.rs @@ -84,7 +84,7 @@ impl ResultContractRef { } /// Canonical outcome family for an admitted application operation. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case", tag = "outcome", content = "value")] pub enum ApplicationOutcome { Evidence(EvidencePacket), diff --git a/crates/tracedecay-contracts/src/result/evidence.rs b/crates/tracedecay-contracts/src/result/evidence.rs index 5894d133a1..c69f765ee6 100644 --- a/crates/tracedecay-contracts/src/result/evidence.rs +++ b/crates/tracedecay-contracts/src/result/evidence.rs @@ -15,7 +15,6 @@ use crate::identity::application_identifier; use super::{CancellationObservation, OperationBudgetUsage, OperationReceipt, ResultContractRef}; application_identifier!( - @no_schema EvidenceIdentity => ("evidence identity", 512), ScoreId => ("score id", 512), // Existing authenticated query cursors bind typed scope, access, key, @@ -26,7 +25,7 @@ application_identifier!( /// Exact continuation authority for the enclosing evidence page: an opaque /// cursor bound to the operation and request that minted it. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum PageCursor { Opaque { cursor: OpaqueCursor }, @@ -47,7 +46,9 @@ impl From for PageCursor { } /// Application-level freshness. Missing or partial truth never becomes current. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(rename_all = "snake_case")] pub enum FreshnessState { Current, @@ -56,7 +57,7 @@ pub enum FreshnessState { } /// Temporal provenance for a packet. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct TemporalState { pub requested_mode: TemporalModeV1, @@ -363,7 +364,9 @@ pub struct Omission { } /// Evidence score semantics. Scores are metadata, never authority. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(rename_all = "snake_case")] pub enum EvidenceScoreKind { OrdinalRank, @@ -372,7 +375,7 @@ pub enum EvidenceScoreKind { CalibratedInterval, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case", tag = "kind")] pub enum EvidenceScoreValue { Ordinal { @@ -387,7 +390,7 @@ pub enum EvidenceScoreValue { }, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct EvidenceScore { pub score_id: ScoreId, @@ -399,7 +402,7 @@ pub struct EvidenceScore { } /// Evidence authority is separate from caller authority and cannot grant access. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct EvidenceAuthority { pub evidence_id: EvidenceIdentity, @@ -411,7 +414,9 @@ pub struct EvidenceAuthority { } /// Bounded elapsed-work classification supplied by a retriever. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(rename_all = "snake_case")] pub enum BudgetClass { WithinBudget, @@ -420,7 +425,9 @@ pub enum BudgetClass { } /// Terminal state reported by one retriever contribution. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord, Hash, +)] #[serde(rename_all = "snake_case")] pub enum RetrieverContributionState { Completed, @@ -434,7 +441,7 @@ pub enum RetrieverContributionState { } /// One source-owned contribution to the packet fold. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct RetrieverContribution { pub retriever_id: RetrieverId, @@ -453,7 +460,7 @@ pub struct RetrieverContribution { /// Stable page state. General cursor bytes stay opaque until authorization is /// revalidated; structural fact cursors retain their canonical ordering type. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct PageState { pub sort_contract_id: SortContractId, @@ -510,7 +517,7 @@ pub struct RetrievalEvidence { } /// Immutable evidence packet consumed by adapters and later planner work. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct EvidencePacket { pub temporal: TemporalState, diff --git a/crates/tracedecay-contracts/src/result/receipt.rs b/crates/tracedecay-contracts/src/result/receipt.rs index 155f9808a5..42173d9252 100644 --- a/crates/tracedecay-contracts/src/result/receipt.rs +++ b/crates/tracedecay-contracts/src/result/receipt.rs @@ -188,7 +188,7 @@ pub enum ReconciliationState { } /// Read-only preview of a future typed effect. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct PreviewResult { pub preview_id: PreviewId, diff --git a/crates/tracedecay-contracts/src/settings_preview.rs b/crates/tracedecay-contracts/src/settings_preview.rs index b193cdd58c..001d51fadc 100644 --- a/crates/tracedecay-contracts/src/settings_preview.rs +++ b/crates/tracedecay-contracts/src/settings_preview.rs @@ -4,10 +4,11 @@ //! operation. This module owns only project patch validation shared before //! adapters invoke that operation. +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// One field-level settings validation failure. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct SettingsValidationIssueV1 { pub field: String, pub message: String, diff --git a/crates/tracedecay-dashboard-api/src/application_surface.rs b/crates/tracedecay-dashboard-api/src/application_surface.rs index 8a2165857e..d6d5a6c9d8 100644 --- a/crates/tracedecay-dashboard-api/src/application_surface.rs +++ b/crates/tracedecay-dashboard-api/src/application_surface.rs @@ -9,7 +9,7 @@ use axum::Router; use axum::extract::Json; use axum::http::StatusCode; use serde_json::Value; -use serde_json::json; +use tracedecay_api::configuration::{ConfigurationRefusalV1, DashboardConfigurationRefusalV1}; use tracedecay_contracts::{ ApplicationContractError, ApplicationOutcome, ApplicationProblemEnvelope, AuthorizedScopeSet, NativeIntegrationSurfaceResultV1, RequestId, @@ -68,10 +68,11 @@ pub(crate) fn configuration_apply_error( } DashboardConfigurationApplyError::ApplicationContractViolation(_) => ( StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ - "code": "application_contract_violation", - "detail": "the configuration application result violated its contract", - })), + Json(DashboardConfigurationRefusalV1::Configuration( + ConfigurationRefusalV1::ApplicationContractViolation { + detail: "the configuration application result violated its contract".to_owned(), + }, + )), ), } } diff --git a/crates/tracedecay-dashboard-api/src/automation_config_api.rs b/crates/tracedecay-dashboard-api/src/automation_config_api.rs index 680372d3a5..4d704e4283 100644 --- a/crates/tracedecay-dashboard-api/src/automation_config_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_config_api.rs @@ -14,7 +14,7 @@ use tracedecay_api::configuration::{ DashboardConfigurationRouteErrorV1, configuration_authority_unavailable_error, configuration_revision_conflict_error, settings_validation_error, }; -use tracedecay_contracts::ApplicationOutcome; +use tracedecay_contracts::{ApplicationOutcome, SettingsValidationIssueV1}; use tracedecay_domain::ProjectId; use tracedecay_domain::configuration::{ AUTOMATION_SETTINGS_SETTING_KEY, ConfigurationIdempotencyKey, ConfigurationLayerIdV1, @@ -70,24 +70,26 @@ pub async fn patch_config( request: std::result::Result, JsonRejection>, ) -> ApiResult { let Json(request) = request.map_err(|error| { - settings_validation_error(json!([{ - "field": "request", - "message": error.body_text(), - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "request".to_owned(), + message: error.body_text(), + }]) })?; let expected_revision = ConfigurationRevisionId::new(request.expected_revision_id).map_err(|_| { - settings_validation_error(json!([{ - "field": "expected_revision_id", - "message": "expected_revision_id must name one canonical configuration revision" - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "expected_revision_id".to_owned(), + message: "expected_revision_id must name one canonical configuration revision" + .to_owned(), + }]) })?; let idempotency_key = ConfigurationIdempotencyKey::new(request.idempotency_key).map_err(|_| { - settings_validation_error(json!([{ - "field": "idempotency_key", - "message": "idempotency_key must be one non-empty canonical caller-stable value" - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "idempotency_key".to_owned(), + message: "idempotency_key must be one non-empty canonical caller-stable value" + .to_owned(), + }]) })?; let (current_revision, current, _) = effective_automation_config(&state) .map_err(|_| configuration_authority_unavailable_error())?; @@ -99,10 +101,10 @@ pub async fn patch_config( )); } let candidate = effective_config(¤t, Some(&request.patch)).map_err(|error| { - settings_validation_error(json!([{ - "field": "automation", - "message": error.to_string(), - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "automation".to_owned(), + message: error.to_string(), + }]) })?; let application_outcome = if candidate == current { diff --git a/crates/tracedecay-dashboard-api/src/contract_schema.rs b/crates/tracedecay-dashboard-api/src/contract_schema.rs index c94f774e50..723cc82be6 100644 --- a/crates/tracedecay-dashboard-api/src/contract_schema.rs +++ b/crates/tracedecay-dashboard-api/src/contract_schema.rs @@ -80,11 +80,13 @@ use super::read_model::{DASHBOARD_SCHEMA_REVISION_V1, DashboardEnvelopeV1}; use super::remote_status_api::RemoteOperationalStatusPayloadV1; use super::savings_api::{SavingsModelsPayloadV1, SavingsOverviewPayloadV1}; use super::settings_api::{ - CodeIndexWorkerSettingsPatch, ProjectSettingsPatch, SettingsPayloadV1, UserSettingsPatch, + CodeIndexWorkerSettingsPatch, ProjectSettingsPatch, ProjectSettingsPatchResponseV1, + SettingsPayloadV1, UserSettingsPatch, }; use super::storage_telemetry_api::StorageTelemetryPayloadV1; use super::work_api::registered_route_contracts as registered_work_route_contracts; use crate::application::feedback::observations::FeedbackObservationReadModelV1; +use tracedecay_api::configuration::DashboardConfigurationRefusalV1; use tracedecay_contracts::code_index_freshness::CodeIndexFreshnessPayloadV1; #[derive(JsonSchema)] @@ -142,6 +144,8 @@ struct DashboardContractCatalogV1 { code_index_freshness: CodeIndexFreshnessPayloadV1, settings: SettingsPayloadV1, settings_project_patch: ProjectSettingsPatch, + settings_project_patch_response: ProjectSettingsPatchResponseV1, + settings_refusal: DashboardConfigurationRefusalV1, settings_user_patch: UserSettingsPatch, settings_code_index_worker_patch: CodeIndexWorkerSettingsPatch, observatory: ObservatoryReadModelV1, diff --git a/crates/tracedecay-dashboard-api/src/settings_api.rs b/crates/tracedecay-dashboard-api/src/settings_api.rs index 5d69f5d0b1..09ee62a729 100644 --- a/crates/tracedecay-dashboard-api/src/settings_api.rs +++ b/crates/tracedecay-dashboard-api/src/settings_api.rs @@ -9,7 +9,7 @@ use axum::Json; use axum::extract::State; use schemars::JsonSchema; use serde::Serialize; -use serde_json::{Value, json}; +use serde_json::Value; use tracedecay_api::configuration::{ DashboardConfigurationRouteErrorV1, PROJECT_SETTINGS_APPLY_OPERATION, SETTINGS_REFRESH_OPERATION, admit_user_settings_patch, configuration_application_problem_error, @@ -20,6 +20,7 @@ use tracedecay_api::configuration::{ }; use tracedecay_contracts::{ ApplicationOutcome, ApplicationProblemEnvelope, ApplicationProblemKind, + SettingsValidationIssueV1, }; use super::DashboardState; @@ -139,7 +140,7 @@ pub struct SettingsPayloadV1 { restart_recommended: Option, } -#[derive(Serialize)] +#[derive(Clone, Debug, Serialize, JsonSchema)] pub struct ProjectSettingsPatchResponseV1 { /// Exact application settlement returned by the configuration binding. /// `None` means the submitted patch was already the current state. @@ -331,10 +332,11 @@ pub async fn patch_project_settings( let pr_autotrack = pr_autotrack_payload(&state)?; let idempotency_key = ConfigurationIdempotencyKey::new(patch.idempotency_key.clone()).map_err(|_| { - settings_validation_error(json!([{ - "field": "idempotency_key", - "message": "idempotency_key must be one non-empty canonical caller-stable value" - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "idempotency_key".to_owned(), + message: "idempotency_key must be one non-empty canonical caller-stable value" + .to_owned(), + }]) })?; let current = state .cached_runtime_configuration() @@ -402,17 +404,19 @@ pub async fn patch_user_settings( let admitted = admit_user_settings_patch(&patch, parse_duration_millis)?; let idempotency_key = ConfigurationIdempotencyKey::new(patch.idempotency_key.clone()).map_err(|_| { - settings_validation_error(json!([{ - "field": "idempotency_key", - "message": "idempotency_key must be one non-empty canonical caller-stable value" - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "idempotency_key".to_owned(), + message: "idempotency_key must be one non-empty canonical caller-stable value" + .to_owned(), + }]) })?; let expected_revision = ConfigurationRevisionId::new(patch.expected_revision_id).map_err(|_| { - settings_validation_error(json!([{ - "field": "expected_revision_id", - "message": "expected_revision_id must name one canonical configuration revision" - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "expected_revision_id".to_owned(), + message: "expected_revision_id must name one canonical configuration revision" + .to_owned(), + }]) })?; let runtime = state .application_invocation_executor @@ -489,17 +493,19 @@ pub async fn patch_code_index_worker_settings( } let idempotency_key = ConfigurationIdempotencyKey::new(patch.idempotency_key.clone()).map_err(|_| { - settings_validation_error(json!([{ - "field": "idempotency_key", - "message": "idempotency_key must be one non-empty canonical caller-stable value" - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "idempotency_key".to_owned(), + message: "idempotency_key must be one non-empty canonical caller-stable value" + .to_owned(), + }]) })?; let expected_revision = ConfigurationRevisionId::new(patch.expected_revision_id).map_err(|_| { - settings_validation_error(json!([{ - "field": "expected_revision_id", - "message": "expected_revision_id must name one canonical configuration revision" - }])) + settings_validation_error([SettingsValidationIssueV1 { + field: "expected_revision_id".to_owned(), + message: "expected_revision_id must name one canonical configuration revision" + .to_owned(), + }]) })?; let port = state .profile_code_index_worker_settings @@ -545,28 +551,28 @@ pub async fn patch_code_index_worker_settings( fn code_index_worker_admission_errors( selection: &CodeIndexWorkerSelectionV1, status: Option<&CodeIndexWorkerStatusV1>, -) -> Vec { +) -> Vec { let (CodeIndexWorkerSelectionV1::Exact { workers }, Some(status)) = (selection, status) else { return Vec::new(); }; let mut errors = Vec::new(); if *workers > status.available_logical_cpus { - errors.push(json!({ - "field": "code_index_workers", - "message": format!( + errors.push(SettingsValidationIssueV1 { + field: "code_index_workers".to_owned(), + message: format!( "code_index_workers exact mode must request no more than {} available logical CPUs", status.available_logical_cpus, ), - })); + }); } if *workers > status.memory_safe_workers { - errors.push(json!({ - "field": "code_index_workers", - "message": format!( + errors.push(SettingsValidationIssueV1 { + field: "code_index_workers".to_owned(), + message: format!( "code_index_workers exact mode must request no more than {} memory-safe workers", status.memory_safe_workers, ), - })); + }); } errors } @@ -865,6 +871,8 @@ fn project_apply_error( #[cfg(test)] mod tests { + mod wire; + use super::*; use serde_json::json; use tracedecay_configuration::config::registry::ConfigurationRegistry; @@ -1023,10 +1031,10 @@ mod tests { &CodeIndexWorkerSelectionV1::Exact { workers: 5 }, Some(&status), ), - vec![json!({ - "field": "code_index_workers", - "message": "code_index_workers exact mode must request no more than 4 available logical CPUs", - })] + vec![SettingsValidationIssueV1 { + field: "code_index_workers".to_owned(), + message: "code_index_workers exact mode must request no more than 4 available logical CPUs".to_owned(), + }] ); } @@ -1077,10 +1085,12 @@ mod tests { &CodeIndexWorkerSelectionV1::Exact { workers: 5 }, Some(&status), ), - vec![json!({ - "field": "code_index_workers", - "message": "code_index_workers exact mode must request no more than 4 memory-safe workers", - })] + vec![SettingsValidationIssueV1 { + field: "code_index_workers".to_owned(), + message: + "code_index_workers exact mode must request no more than 4 memory-safe workers" + .to_owned(), + }] ); } } diff --git a/crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs b/crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs new file mode 100644 index 0000000000..47682ebfde --- /dev/null +++ b/crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs @@ -0,0 +1,272 @@ +//! Serialized producer fixtures shared with the dashboard behavior tests. + +use axum::Json; +use axum::body::to_bytes; +use axum::response::IntoResponse; +use serde_json::{Value, json}; +use tracedecay_api::configuration::{ + configuration_application_problem_error, configuration_authority_unavailable_error, + configuration_revision_conflict_error, parse_project_settings_patch, +}; +use tracedecay_api::read_model::{ + DashboardCoverageV1, DashboardEnvelopeV1, DashboardLegalActionKindV1, + DashboardLegalActionRefV1, DashboardScopeV1, +}; +use tracedecay_contracts::{ + ApplicationOutcome, ApplicationProblem, ApplicationProblemEnvelope, AuthorityReceipt, + CapabilityGrantId, Deadline, DisclosureClass, EffectId, EffectReceipt, EffectResult, + EffectTermination, IdempotencyKey, OperationBudgetUsage, OperationReceipt, PolicyDecisionRef, + ReconciliationState, RequestId, ResolvedScope, ResultContractRef, +}; +use tracedecay_domain::configuration::{CodeIndexWorkerSelectionV1, ConfigurationRevisionId}; +use tracedecay_domain::{ + ActorId, ComponentVersion, ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, +}; +use tracedecay_tool_catalog::{EffectClass, SchemaId, UseCaseId}; + +use super::pinned_defaults; +use crate::settings_api::{ + EnvironmentSettingsPayloadV1, PrAutoTrackPayloadV1, ProjectSettingsPatchResponseV1, + ProjectSettingsPayloadV1, SettingsPayloadV1, StorageSettingsPayloadV1, UserSettingsPayloadV1, + VersionSettingsPayloadV1, automation_settings_payload, project_editable_settings, +}; + +fn settings(revision: &str, max_file_size: u64) -> DashboardEnvelopeV1 { + let pinned = pinned_defaults(&ConfigurationRevisionId::new(revision).unwrap()); + let mut config = project_editable_settings(&pinned); + config.max_file_size = max_file_size; + let payload = SettingsPayloadV1 { + project: ProjectSettingsPayloadV1 { + configuration_snapshot_id: pinned.snapshot().snapshot_id.as_str().to_owned(), + configuration_revision_id: revision.to_owned(), + config, + pr_autotrack: PrAutoTrackPayloadV1 { tracked: vec![] }, + }, + user: UserSettingsPayloadV1 { + configuration_snapshot_id: "user-snapshot-7".to_owned(), + configuration_revision_id: "user-rev-7".to_owned(), + code_index_worker_configuration_snapshot_id: "workers-snapshot-3".to_owned(), + code_index_worker_configuration_revision_id: "workers-rev-3".to_owned(), + upload_enabled: false, + code_index_workers: CodeIndexWorkerSelectionV1::Automatic {}, + code_index_worker_status: None, + watcher_debounce: "15s".to_owned(), + extraction_timeout_secs: 30, + installed_agents: vec![], + }, + automation: automation_settings_payload(&pinned), + environment: EnvironmentSettingsPayloadV1 { + global_accounting_mode: "disabled".to_owned(), + global_accounting_enabled: false, + pricing_offline: true, + variables: vec![], + }, + storage: StorageSettingsPayloadV1 { + project_id: Some("project.settings.scout".to_owned()), + project_root: "/project".to_owned(), + storage_mode: "project_local".to_owned(), + store_root: "/project/.tracedecay".to_owned(), + dashboard_root: "/project".to_owned(), + graph_db: "/project/.tracedecay/graph.db".to_owned(), + memory_db: "/project/.tracedecay/memory.db".to_owned(), + lcm_db: "/project/.tracedecay/lcm.db".to_owned(), + lcm_scope: "project".to_owned(), + savings_db: "/project/.tracedecay/savings.db".to_owned(), + }, + version: VersionSettingsPayloadV1 { + version: "0.1.0".to_owned(), + channel: "stable".to_owned(), + cached_latest_version: None, + }, + resync_recommended: Some(max_file_size != 1_048_576), + restart_recommended: None, + }; + let mut envelope = DashboardEnvelopeV1::ready( + DashboardScopeV1 { + project_id: Some("project.settings.scout".to_owned()), + storage_mode: "project_local".to_owned(), + store_root: "/project/.tracedecay".to_owned(), + }, + DashboardCoverageV1::complete(1, "configuration_control_plane"), + payload, + ) + .with_legal_actions(vec![ + DashboardLegalActionRefV1::new( + DashboardLegalActionKindV1::RequestApply, + "configuration_batch", + ), + DashboardLegalActionRefV1::new(DashboardLegalActionKindV1::Refresh, "configuration_list"), + ]); + envelope.time.observation_time_micros = 100; + envelope.freshness.observed_at_micros = Some(100); + envelope +} + +fn digest(character: char) -> ManifestDigest { + ManifestDigest::new(format!("sha256:{}", character.to_string().repeat(64))).unwrap() +} + +fn effect() -> ApplicationOutcome { + let scope = ResolvedScope::new( + ProjectId::new("project.settings.scout").unwrap(), + RepositoryId::new("repository.settings").unwrap(), + WorktreeId::new("worktree.settings").unwrap(), + None, + ) + .unwrap(); + let idempotency_key = IdempotencyKey::new("settings-idempotency-fixture").unwrap(); + let authority = AuthorityReceipt { + grant_id: CapabilityGrantId::new("grant.settings").unwrap(), + grant_revision: 1, + grant_digest: digest('a'), + authorized_scope_digest: scope.scope_digest.clone(), + disclosure: DisclosureClass::Metadata, + policy: PolicyDecisionRef::new( + "policy.settings", + 1, + digest('b'), + ComponentVersion::new("policy.settings.v1").unwrap(), + ) + .unwrap(), + revalidated_at: UtcMicros(10), + }; + authority.validate_for(&scope).unwrap(); + ApplicationOutcome::Effect( + EffectResult::new( + EffectId::new("effect.configuration.settings").unwrap(), + EffectClass::Administrative, + idempotency_key.clone(), + authority, + digest('c'), + OperationReceipt::completed( + UtcMicros(10), + UtcMicros(20), + Deadline::new(UtcMicros(1_000)).unwrap(), + OperationBudgetUsage::default(), + ) + .unwrap(), + ReconciliationState::Reconciled, + EffectReceipt { + operation: UseCaseId::new("use-case.configuration.batch").unwrap(), + request_id: RequestId::new("request.configuration.settings-fixture").unwrap(), + actor: ActorId::new("actor.settings").unwrap(), + scope, + effect_class: EffectClass::Administrative, + idempotency_key, + input_digest: digest('a'), + expected_state: digest('c'), + policy_digest: digest('b'), + configuration_digest: digest('d'), + catalog_digest: digest('e'), + privacy_digest: digest('f'), + outcome: EffectTermination::Completed, + committed_state: Some(digest('1')), + external_proof: None, + }, + None, + ) + .unwrap(), + ) +} + +async fn http_fixture(response: impl IntoResponse) -> Value { + let response = response.into_response(); + let status = response.status().as_u16(); + let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + json!({ "status": status, "body": body }) +} + +#[tokio::test] +async fn project_patch_serialization_preserves_settlement_and_refusals() { + let outcome = effect(); + let expected_receipt = match &outcome { + ApplicationOutcome::Effect(effect) => serde_json::to_value(&effect.receipt).unwrap(), + _ => unreachable!("fixture constructs an effect"), + }; + let changed = http_fixture(Json(ProjectSettingsPatchResponseV1 { + application_outcome: Some(outcome), + current: settings("rev-43", 2_097_152), + })) + .await; + assert_eq!(changed["status"], 200); + assert_eq!(changed["body"]["application_outcome"]["outcome"], "effect"); + assert_eq!( + changed["body"]["application_outcome"]["value"]["receipt"], + expected_receipt + ); + assert_eq!( + changed["body"]["current"]["payload"]["project"]["configuration_revision_id"], + "rev-43" + ); + let unchanged = http_fixture(Json(ProjectSettingsPatchResponseV1 { + application_outcome: None, + current: settings("rev-42", 1_048_576), + })) + .await; + assert!( + unchanged["body"] + .as_object() + .unwrap() + .contains_key("application_outcome") + ); + assert!(unchanged["body"]["application_outcome"].is_null()); + + let idempotency_conflict = ApplicationProblemEnvelope::new( + ResultContractRef::new( + SchemaId::new("schema.configuration.batch.result").unwrap(), + 1, + ) + .unwrap(), + RequestId::new("request.configuration.settings-fixture").unwrap(), + ApplicationProblem::conflict( + "configuration.conflict", + "The idempotency key was already used for a different settings change.", + ), + ) + .unwrap(); + let expected_problem = serde_json::to_value(&idempotency_conflict).unwrap(); + let idempotency = http_fixture(configuration_application_problem_error( + idempotency_conflict, + )) + .await; + assert_eq!(idempotency["status"], 409); + assert_eq!(idempotency["body"], expected_problem); + + let validation = parse_project_settings_patch(json!({ + "expected_revision_id": "rev-42", + "idempotency_key": "settings-idempotency-fixture", + "unknown_setting": true, + })) + .expect_err("unknown fields must be refused"); + let cancelled = ApplicationProblemEnvelope::new( + ResultContractRef::new( + SchemaId::new("schema.configuration.batch.result").unwrap(), + 1, + ) + .unwrap(), + RequestId::new("request.configuration.settings-fixture").unwrap(), + ApplicationProblem::cancelled_before_admission(), + ) + .unwrap(); + let fixtures = json!({ + "read": http_fixture(Json(settings("rev-42", 1_048_576))).await, + "changed": changed, + "unchanged": unchanged, + "revision_conflict": http_fixture(configuration_revision_conflict_error( + "settings changed after this edit began; refresh and retry", "rev-42", "rev-43" + )).await, + "validation": http_fixture(validation).await, + "unavailable": http_fixture(configuration_authority_unavailable_error()).await, + "idempotency_conflict": idempotency, + "cancelled": http_fixture(configuration_application_problem_error(cancelled)).await, + }); + // Bazel preserves this producer output for the frontend's shared fixture. + if let Some(output) = std::env::var_os("TEST_UNDECLARED_OUTPUTS_DIR") { + std::fs::write( + std::path::PathBuf::from(output).join("settings-patch.json"), + format!("{}\n", serde_json::to_string_pretty(&fixtures).unwrap()), + ) + .unwrap(); + } +} diff --git a/dashboard/codegen/src/generate.ts b/dashboard/codegen/src/generate.ts index 3ecd2815cd..1c3d4fc890 100644 --- a/dashboard/codegen/src/generate.ts +++ b/dashboard/codegen/src/generate.ts @@ -167,6 +167,7 @@ function isTaggedUnion(schema: JsonSchema): boolean { interface ResolveCtx { genericParam?: string; genericSchemaVar?: string; + envelopePayloads: ReadonlyMap; } interface Resolved { @@ -197,6 +198,16 @@ function resolveType(schema: JsonSchema, ctx: ResolveCtx): Resolved { if (schema.$ref) { const name = refName(schema.$ref); + const payload = ctx.envelopePayloads.get(name); + if (payload) { + const resolved = resolveType(payload, ctx); + const ts = `DashboardEnvelopeV1<${resolved.ts}>`; + return { + ts, + zod: `z.lazy(() => DashboardEnvelopeV1Schema(${resolved.zod}))`, + ann: typeAnn(ts), + }; + } return { ts: name, zod: `z.lazy(() => ${name}Schema)`, ann: typeAnn(name) }; } @@ -393,14 +404,18 @@ interface EmittedDef { decoders: string; } -function emitNamedDef(name: string, schema: JsonSchema): EmittedDef { +function emitNamedDef( + name: string, + schema: JsonSchema, + envelopePayloads: ReadonlyMap, +): EmittedDef { const genericParam = schema["x-generic"]; const desc = schema.description ? `/** ${schema.description} */\n` : ""; if (genericParam) { // Generic type + decoder factory (DashboardEnvelope). const schemaVar = "payloadSchema"; - const ctx: ResolveCtx = { genericParam, genericSchemaVar: schemaVar }; + const ctx: ResolveCtx = { genericParam, genericSchemaVar: schemaVar, envelopePayloads }; const obj = resolveObject(schema, ctx); return { typeName: name, @@ -415,7 +430,7 @@ function emitNamedDef(name: string, schema: JsonSchema): EmittedDef { }; } - const ctx: ResolveCtx = {}; + const ctx: ResolveCtx = { envelopePayloads }; if (isTaggedUnion(schema)) { const union = resolveTaggedUnion(schema, ctx); @@ -457,12 +472,14 @@ export function generateContracts(bundles: JsonSchema[]): GeneratedContracts { const schemaRevision = revisions[0] as string | number; const omittedNames = new Set(["DashboardPayloadMarkerV1"]); + const envelopePayloads = new Map(); const envelopeIndex = defs.findIndex(([name]) => name === "DashboardEnvelopeV1"); const envelope = defs[envelopeIndex]?.[1]; if (envelope?.properties) { for (const [name, schema] of defs) { if (name !== "DashboardEnvelopeV1" && isMonomorphizedEnvelope(schema, envelope)) { omittedNames.add(name); + envelopePayloads.set(name, schema.properties!.payload!); } } defs[envelopeIndex] = [ @@ -482,13 +499,15 @@ export function generateContracts(bundles: JsonSchema[]): GeneratedContracts { const emitted: EmittedDef[] = []; for (const [name, schema] of defs) { if (omittedNames.has(name)) continue; - const omittedRef = [...localDefinitionRefs(schema)].find((ref) => omittedNames.has(ref)); + const omittedRef = [...localDefinitionRefs(schema)].find( + (ref) => omittedNames.has(ref) && !envelopePayloads.has(ref), + ); if (omittedRef) { throw new Error( `${name} references omitted generated definition ${omittedRef}`, ); } - emitted.push(emitNamedDef(name, schema)); + emitted.push(emitNamedDef(name, schema, envelopePayloads)); } const types = [ diff --git a/dashboard/codegen/tests/generate.test.ts b/dashboard/codegen/tests/generate.test.ts index bcae67b313..d6ba9dd293 100644 --- a/dashboard/codegen/tests/generate.test.ts +++ b/dashboard/codegen/tests/generate.test.ts @@ -164,14 +164,14 @@ describe("contracts generator", () => { expect(generated).toContain("export const DashboardEnvelopeV1MetadataSchema"); }); - it("rejects an emitted contract that still references an omitted envelope instance", () => { + it("rejects an emitted contract that references the untyped payload marker", () => { const bundle = structuredClone(bundles[0]!); bundle.$defs!.EnvelopeConsumer = { - $ref: "#/$defs/DashboardEnvelopeV12", + $ref: "#/$defs/DashboardPayloadMarkerV1", }; expect(() => generateContracts([bundle])).toThrow( - "EnvelopeConsumer references omitted generated definition DashboardEnvelopeV12", + "EnvelopeConsumer references omitted generated definition DashboardPayloadMarkerV1", ); }); @@ -216,6 +216,28 @@ describe("contracts generator", () => { const bundle: JsonSchema = { schemaRevision: "test.1", $defs: { + DashboardPayloadMarkerV1: { type: "null" }, + DashboardEnvelopeV1: { + type: "object", + properties: { + schema_revision: { type: "string" }, + payload: { $ref: "#/$defs/DashboardPayloadMarkerV1" }, + }, + required: ["schema_revision", "payload"], + }, + NodeEnvelope: { + type: "object", + properties: { + schema_revision: { type: "string" }, + payload: { $ref: "#/$defs/Node" }, + }, + required: ["schema_revision", "payload"], + }, + EnvelopeConsumer: { + type: "object", + properties: { current: { $ref: "#/$defs/NodeEnvelope" } }, + required: ["current"], + }, Node: { type: "object", additionalProperties: false, @@ -278,12 +300,15 @@ import { ChoiceSchema, ClosedReadingSchema, DashboardDomainStateV1Schema, + EnvelopeConsumerSchema, NodeSchema, OpenReadingSchema, ResultSchema, StatusSchema, type Choice, type ClosedReading, + type DashboardEnvelopeV1, + type EnvelopeConsumer, type Node, type OpenReading, type Result, @@ -293,6 +318,10 @@ import { const catchInput: z.input = 42; const valid: Node = { id: "root", label: null, metadata: null, child: { id: "leaf", label: "ok", metadata: {} } }; const decoded: Node = NodeSchema.parse(valid); +const nested: EnvelopeConsumer = { current: { schema_revision: "test.1", payload: valid } }; +const envelope: DashboardEnvelopeV1 = EnvelopeConsumerSchema.parse(nested).current; +// @ts-expect-error a nested envelope keeps its payload contract +const invalidNested: EnvelopeConsumer = { current: { schema_revision: "test.1", payload: { id: 42 } } }; const inferred: z.infer = valid; // @ts-expect-error required nullable fields cannot be omitted const missing: Node = { id: "root" }; @@ -315,7 +344,7 @@ function unwrap(result: Result): Node | undefined { default: return assertNever(result); } } -export { catchInput, decoded, inferred, missing, invalidChild, invalidStatus, closedMissing, closed, open, extended, field, status, choice, unwrap }; +export { catchInput, decoded, nested, envelope, invalidNested, inferred, missing, invalidChild, invalidStatus, closedMissing, closed, open, extended, field, status, choice, unwrap }; `); checkContractTypes(directory, ["decoders.ts", "consumer.ts"]); // Node's loader avoids waiting on Vitest's shared transform server. @@ -327,6 +356,14 @@ export { catchInput, decoded, inferred, missing, invalidChild, invalidStatus, cl child: { id: "leaf", label: "ok", metadata: 1 }, }; expect(runtime.NodeSchema!.parse(node)).toEqual(node); + const nested = { current: { schema_revision: "test.1", payload: node } }; + expect(runtime.EnvelopeConsumerSchema!.parse(nested)).toEqual(nested); + expect(runtime.EnvelopeConsumerSchema!.safeParse({ + current: { schema_revision: "test.1", payload: { ...node, id: 42 } }, + }).success).toBe(false); + expect(runtime.EnvelopeConsumerSchema!.safeParse({ + current: { schema_revision: "future", payload: node }, + }).success).toBe(false); expect(runtime.NodeSchema!.safeParse({ id: "root", metadata: 1 }).success).toBe(false); expect(runtime.NodeSchema!.safeParse({ ...node, extra: true }).success).toBe(false); expect(runtime.NodeSchema!.safeParse({ From f5e226e8d84c05880ab3386a097a574be3cb4dfc Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:13:17 -0700 Subject: [PATCH 2/6] fix(ci): resolve export paths in the runner steps --- .github/workflows/dashboard-contract-export.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dashboard-contract-export.yml b/.github/workflows/dashboard-contract-export.yml index a4eba1eb74..c9aeab7cda 100644 --- a/.github/workflows/dashboard-contract-export.yml +++ b/.github/workflows/dashboard-contract-export.yml @@ -20,7 +20,6 @@ jobs: timeout-minutes: 90 env: CARGO_BAZEL_TIMEOUT: "3600" - EXPORT_DIRECTORY: ${{ runner.temp }}/dashboard-contract-export steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -40,6 +39,7 @@ jobs: - name: Export the canonical dashboard schema run: | set -euo pipefail + EXPORT_DIRECTORY="$RUNNER_TEMP/dashboard-contract-export" mkdir -p "$EXPORT_DIRECTORY" bazel run --config=ci --jobs=1 //sdks/codegen:dashboard_schema -- "$EXPORT_DIRECTORY/dashboard-contracts.schema.json" test -s "$EXPORT_DIRECTORY/dashboard-contracts.schema.json" @@ -56,6 +56,7 @@ jobs: - name: Serialize the real settings response fixture run: | set -euo pipefail + EXPORT_DIRECTORY="$RUNNER_TEMP/dashboard-contract-export" bazel test --config=ci --jobs=1 //crates/tracedecay-dashboard-api:unit_test \ --test_arg=settings_api::tests::wire::project_patch_serialization_preserves_settlement_and_refusals \ --test_arg=--exact --test_output=all --cache_test_results=no \ From 451721797b8ae53bc539072fc19994df9cb66d34 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 18 Oct 2026 20:27:12 +0000 Subject: [PATCH 3/6] fix(dashboard): decode canonical settings settlements --- .../workflows/dashboard-contract-export.yml | 74 -- .../schemas/dashboard-contracts.schema.json | 1105 +++++++++++++++++ dashboard/codegen/src/generate.ts | 2 +- dashboard/src/contracts/decoders.ts | 401 ++++++ dashboard/src/contracts/types.ts | 440 +++++-- .../src/test/fixtures/settings-patch.json | 639 ++++++++++ .../settings/SettingsPage.dom.test.tsx | 40 +- .../settings/SettingsReviewPanel.tsx | 5 +- .../settings/settingsEditorMachine.ts | 4 +- .../settings/settingsMutation.test.ts | 89 ++ .../workspaces/settings/settingsMutation.ts | 155 +-- 11 files changed, 2673 insertions(+), 281 deletions(-) delete mode 100644 .github/workflows/dashboard-contract-export.yml create mode 100644 dashboard/src/test/fixtures/settings-patch.json create mode 100644 dashboard/src/workspaces/settings/settingsMutation.test.ts diff --git a/.github/workflows/dashboard-contract-export.yml b/.github/workflows/dashboard-contract-export.yml deleted file mode 100644 index c9aeab7cda..0000000000 --- a/.github/workflows/dashboard-contract-export.yml +++ /dev/null @@ -1,74 +0,0 @@ -# Temporary build route: remove after importing the canonical generated JSON. -name: Export dashboard contract artifacts -run-name: Export dashboard contracts for ${{ github.event.pull_request.head.sha }} - -on: - pull_request: - types: [opened, synchronize, reopened] - -permissions: - contents: read - -concurrency: - group: dashboard-contract-export-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - export: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - timeout-minutes: 90 - env: - CARGO_BAZEL_TIMEOUT: "3600" - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false - - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: "26" - - - uses: ./.github/actions/setup-pnpm - - - uses: ./.github/actions/setup-bazel - with: - cache-key: ci-linux-test-v2 - - - name: Export the canonical dashboard schema - run: | - set -euo pipefail - EXPORT_DIRECTORY="$RUNNER_TEMP/dashboard-contract-export" - mkdir -p "$EXPORT_DIRECTORY" - bazel run --config=ci --jobs=1 //sdks/codegen:dashboard_schema -- "$EXPORT_DIRECTORY/dashboard-contracts.schema.json" - test -s "$EXPORT_DIRECTORY/dashboard-contracts.schema.json" - - # Preserve the schema before the independent fixture build starts. - - name: Upload the canonical dashboard schema - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: dashboard-schema-${{ github.event.pull_request.head.sha }} - path: ${{ runner.temp }}/dashboard-contract-export/dashboard-contracts.schema.json - if-no-files-found: error - retention-days: 1 - - - name: Serialize the real settings response fixture - run: | - set -euo pipefail - EXPORT_DIRECTORY="$RUNNER_TEMP/dashboard-contract-export" - bazel test --config=ci --jobs=1 //crates/tracedecay-dashboard-api:unit_test \ - --test_arg=settings_api::tests::wire::project_patch_serialization_preserves_settlement_and_refusals \ - --test_arg=--exact --test_output=all --cache_test_results=no \ - --nozip_undeclared_test_outputs - fixture=bazel-testlogs/crates/tracedecay-dashboard-api/unit_test/test.outputs/settings-patch.json - test -s "$fixture" - cp -- "$fixture" "$EXPORT_DIRECTORY/settings-patch.json" - - - name: Upload the canonical settings response fixture - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: settings-patch-fixture-${{ github.event.pull_request.head.sha }} - path: ${{ runner.temp }}/dashboard-contract-export/settings-patch.json - if-no-files-found: error - retention-days: 1 diff --git a/dashboard/codegen/schemas/dashboard-contracts.schema.json b/dashboard/codegen/schemas/dashboard-contracts.schema.json index e65724f847..76951811fd 100644 --- a/dashboard/codegen/schemas/dashboard-contracts.schema.json +++ b/dashboard/codegen/schemas/dashboard-contracts.schema.json @@ -1343,6 +1343,74 @@ ], "type": "string" }, + "ApplicationOutcome": { + "description": "Canonical outcome family for an admitted application operation.", + "oneOf": [ + { + "properties": { + "outcome": { + "const": "evidence", + "type": "string" + }, + "value": { + "$ref": "#/$defs/EvidencePacket" + } + }, + "required": [ + "outcome", + "value" + ], + "type": "object" + }, + { + "properties": { + "outcome": { + "const": "preview", + "type": "string" + }, + "value": { + "$ref": "#/$defs/PreviewResult" + } + }, + "required": [ + "outcome", + "value" + ], + "type": "object" + }, + { + "properties": { + "outcome": { + "const": "effect", + "type": "string" + }, + "value": { + "$ref": "#/$defs/EffectResult" + } + }, + "required": [ + "outcome", + "value" + ], + "type": "object" + }, + { + "description": "An operation-owned typed result that carries its own receipts, such as\na source-edit result with its durable effect, or a rendered document.", + "properties": { + "outcome": { + "const": "result", + "type": "string" + }, + "value": true + }, + "required": [ + "outcome", + "value" + ], + "type": "object" + } + ] + }, "ApplicationProblemDetailV1": { "description": "The structured facts behind a problem. Adapters read these fields; the\nproblem's `message` is only their one human rendering.", "oneOf": [ @@ -1828,6 +1896,45 @@ "description": "Strongly typed canonical identity: `AttemptId`.", "type": "string" }, + "AuthorityReceipt": { + "additionalProperties": false, + "description": "Proof that this request crossed the current authorization boundary.", + "properties": { + "authorized_scope_digest": { + "$ref": "#/$defs/ManifestDigest" + }, + "disclosure": { + "$ref": "#/$defs/DisclosureClass" + }, + "grant_digest": { + "$ref": "#/$defs/ManifestDigest" + }, + "grant_id": { + "type": "string" + }, + "grant_revision": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "policy": { + "$ref": "#/$defs/PolicyDecisionRef" + }, + "revalidated_at": { + "$ref": "#/$defs/UtcMicros" + } + }, + "required": [ + "grant_id", + "grant_revision", + "grant_digest", + "authorized_scope_digest", + "disclosure", + "policy", + "revalidated_at" + ], + "type": "object" + }, "AuthorizedRoot": { "additionalProperties": false, "description": "One exact application scope paired with its registered physical locator.\n\nThe scope remains the identity authority. The locator is retained only so\na later read or restart can reopen the exact registered root without an\nactive-graph or CWD fallback.", @@ -3345,6 +3452,15 @@ ], "type": "object" }, + "BudgetClass": { + "description": "Bounded elapsed-work classification supplied by a retriever.", + "enum": [ + "within_budget", + "approaching_limit", + "exhausted" + ], + "type": "string" + }, "CallChainMeasurementV1": { "properties": { "complete": { @@ -3452,6 +3568,22 @@ ], "type": "object" }, + "CancellationObservation": { + "additionalProperties": false, + "properties": { + "observed_at": { + "$ref": "#/$defs/UtcMicros" + }, + "stage": { + "$ref": "#/$defs/CancellationStage" + } + }, + "required": [ + "stage", + "observed_at" + ], + "type": "object" + }, "CancellationStage": { "description": "Exact stage at which cancellation or deadline state was observed.", "enum": [ @@ -3813,6 +3945,43 @@ "description": "Strongly typed canonical identity: `CodeGenerationId`.", "type": "string" }, + "CodeGraphReadFreshnessV1": { + "description": "Freshness of the generation a verified graph read serves.", + "oneOf": [ + { + "properties": { + "state": { + "const": "current", + "type": "string" + } + }, + "required": [ + "state" + ], + "type": "object" + }, + { + "properties": { + "rebuild_in_flight": { + "type": "boolean" + }, + "sealed_at": { + "$ref": "#/$defs/UtcMicros" + }, + "state": { + "const": "last_complete_stale", + "type": "string" + } + }, + "required": [ + "state", + "sealed_at", + "rebuild_in_flight" + ], + "type": "object" + } + ] + }, "CodeGraphServingReadinessV1": { "description": "Interactive graph-serving state for the latest sealed generation.\n\nA sealed generation can expose truthful census statistics before its graph\nprojection is ready to serve queries, so readiness is reported separately.", "oneOf": [ @@ -4752,6 +4921,70 @@ "description": "Strongly typed canonical identity: `ComponentVersion`.", "type": "string" }, + "ConfigurationRefusalV1": { + "description": "Configuration refusals carrying the same stable codes as the HTTP routes.", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "actual_revision_id": { + "type": "string" + }, + "code": { + "const": "configuration_revision_conflict", + "type": "string" + }, + "detail": { + "type": "string" + }, + "expected_revision_id": { + "type": "string" + } + }, + "required": [ + "code", + "detail", + "expected_revision_id", + "actual_revision_id" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "configuration_authority_unavailable", + "type": "string" + }, + "detail": { + "type": "string" + } + }, + "required": [ + "code", + "detail" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "application_contract_violation", + "type": "string" + }, + "detail": { + "type": "string" + } + }, + "required": [ + "code", + "detail" + ], + "type": "object" + } + ] + }, "ConfigurationRevisionId": { "description": "Strongly typed canonical identity: `ConfigurationRevisionId`.", "type": "string" @@ -5167,6 +5400,36 @@ } ] }, + "DashboardConfigurationRefusalV1": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "detail": { + "type": "string" + }, + "validation_errors": { + "items": { + "$ref": "#/$defs/SettingsValidationIssueV1" + }, + "type": "array" + } + }, + "required": [ + "detail", + "validation_errors" + ], + "type": "object" + }, + { + "$ref": "#/$defs/ConfigurationRefusalV1" + }, + { + "$ref": "#/$defs/ApplicationProblemEnvelope" + } + ], + "description": "Every refusal body produced by the dashboard configuration adapter. The\nuntagged wrapper preserves the existing validation, CAS, and application\nproblem wire formats while giving producers and consumers one authority." + }, "DashboardCoverageCompletenessV1": { "description": "Coverage completeness axis. `Unsupported` distinguishes \"the source that\nwould establish coverage is not wired\" from `Unknown` (\"coverage could not\nbe determined\").", "enum": [ @@ -6917,6 +7180,70 @@ ], "type": "object" }, + "DashboardEnvelopeV130": { + "description": "The normative read-model envelope. Every V2 read-model route returns exactly\nthis shape; only `payload` varies by route.", + "properties": { + "authorization": { + "$ref": "#/$defs/DashboardAuthorizationV1" + }, + "coverage": { + "$ref": "#/$defs/DashboardCoverageV1" + }, + "domain_state": { + "$ref": "#/$defs/DashboardDomainStateV1" + }, + "freshness": { + "$ref": "#/$defs/DashboardFreshnessV1" + }, + "legal_actions": { + "items": { + "$ref": "#/$defs/DashboardLegalActionRefV1" + }, + "type": "array" + }, + "payload": { + "$ref": "#/$defs/SettingsPayloadV1" + }, + "schema_revision": { + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "scope": { + "$ref": "#/$defs/DashboardScopeV1" + }, + "source_watermark": { + "anyOf": [ + { + "$ref": "#/$defs/DashboardWatermarkV1" + }, + { + "type": "null" + } + ] + }, + "time": { + "$ref": "#/$defs/DashboardTimeV1" + }, + "version": { + "$ref": "#/$defs/DashboardVersionV1" + } + }, + "required": [ + "schema_revision", + "scope", + "version", + "time", + "source_watermark", + "authorization", + "coverage", + "freshness", + "domain_state", + "legal_actions", + "payload" + ], + "type": "object" + }, "DashboardEnvelopeV14": { "description": "The normative read-model envelope. Every V2 read-model route returns exactly\nthis shape; only `payload` varies by route.", "properties": { @@ -7487,6 +7814,19 @@ ], "type": "object" }, + "Deadline": { + "additionalProperties": false, + "description": "One immutable deadline supplied by the caller or upstream admission layer.", + "properties": { + "expires_at": { + "$ref": "#/$defs/UtcMicros" + } + }, + "required": [ + "expires_at" + ], + "type": "object" + }, "DecideWorkProposalRequestV1": { "additionalProperties": false, "properties": { @@ -11160,6 +11500,15 @@ ], "type": "object" }, + "DisclosureClass": { + "description": "Disclosure ceiling carried by an immutable grant and revalidated at sinks.", + "enum": [ + "metadata", + "evidence", + "sensitive" + ], + "type": "string" + }, "DoctorCoverageCompletenessV1": { "description": "Whether Doctor observed all of a family's evidence sources.", "oneOf": [ @@ -11729,6 +12078,49 @@ ], "type": "object" }, + "EffectResult": { + "additionalProperties": false, + "description": "Result of an admitted effect. `EffectUnknown` remains a receipt state and\ncannot be remapped into a pre-admission problem.", + "properties": { + "authority": { + "$ref": "#/$defs/AuthorityReceipt" + }, + "effect_class": { + "$ref": "#/$defs/EffectClass" + }, + "effect_id": { + "type": "string" + }, + "execution": { + "$ref": "#/$defs/OperationReceipt" + }, + "expected_state": { + "$ref": "#/$defs/ManifestDigest" + }, + "idempotency_key": { + "type": "string" + }, + "payload": true, + "receipt": { + "$ref": "#/$defs/EffectReceipt" + }, + "reconciliation": { + "$ref": "#/$defs/ReconciliationState" + } + }, + "required": [ + "effect_id", + "effect_class", + "idempotency_key", + "authority", + "expected_state", + "execution", + "reconciliation", + "receipt", + "payload" + ], + "type": "object" + }, "EffectTermination": { "description": "Durable effect receipt terminal state.", "enum": [ @@ -11793,6 +12185,46 @@ ], "type": "object" }, + "EvidenceAuthority": { + "additionalProperties": false, + "description": "Evidence authority is separate from caller authority and cannot grant access.", + "properties": { + "evidence_id": { + "type": "string" + }, + "horizon": { + "anyOf": [ + { + "$ref": "#/$defs/UtcMicros" + }, + { + "type": "null" + } + ] + }, + "producer": { + "type": "string" + }, + "revision": { + "$ref": "#/$defs/ComponentVersion" + }, + "scope": { + "$ref": "#/$defs/ResolvedScope" + }, + "source_kind": { + "type": "string" + } + }, + "required": [ + "evidence_id", + "source_kind", + "producer", + "scope", + "revision", + "horizon" + ], + "type": "object" + }, "EvidenceCoverage": { "additionalProperties": false, "description": "Deterministic coverage fold input for an evidence packet.", @@ -11858,6 +12290,184 @@ ], "type": "string" }, + "EvidencePacket": { + "additionalProperties": false, + "description": "Immutable evidence packet consumed by adapters and later planner work.", + "properties": { + "authority": { + "$ref": "#/$defs/AuthorityReceipt" + }, + "contributions": { + "items": { + "$ref": "#/$defs/RetrieverContribution" + }, + "type": "array" + }, + "coverage": { + "$ref": "#/$defs/EvidenceCoverage" + }, + "evidence_authorities": { + "items": { + "$ref": "#/$defs/EvidenceAuthority" + }, + "type": "array" + }, + "execution": { + "$ref": "#/$defs/OperationReceipt" + }, + "omissions": { + "items": { + "$ref": "#/$defs/Omission" + }, + "type": "array" + }, + "page": { + "$ref": "#/$defs/PageState" + }, + "payload": true, + "scores": { + "items": { + "$ref": "#/$defs/EvidenceScore" + }, + "type": "array" + }, + "temporal": { + "$ref": "#/$defs/TemporalState" + } + }, + "required": [ + "temporal", + "authority", + "evidence_authorities", + "coverage", + "omissions", + "scores", + "contributions", + "page", + "execution", + "payload" + ], + "type": "object" + }, + "EvidenceScore": { + "additionalProperties": false, + "properties": { + "calibration_revision": { + "anyOf": [ + { + "$ref": "#/$defs/ComponentVersion" + }, + { + "type": "null" + } + ] + }, + "calibration_valid": { + "type": [ + "boolean", + "null" + ] + }, + "deterministic_components": { + "items": { + "type": "string" + }, + "type": "array" + }, + "kind": { + "$ref": "#/$defs/EvidenceScoreKind" + }, + "score_id": { + "type": "string" + }, + "value": { + "$ref": "#/$defs/EvidenceScoreValue" + } + }, + "required": [ + "score_id", + "kind", + "value", + "calibration_revision", + "calibration_valid", + "deterministic_components" + ], + "type": "object" + }, + "EvidenceScoreKind": { + "description": "Evidence score semantics. Scores are metadata, never authority.", + "enum": [ + "ordinal_rank", + "heuristic_score", + "calibrated_probability", + "calibrated_interval" + ], + "type": "string" + }, + "EvidenceScoreValue": { + "oneOf": [ + { + "properties": { + "kind": { + "const": "ordinal", + "type": "string" + }, + "rank": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "kind", + "rank" + ], + "type": "object" + }, + { + "properties": { + "kind": { + "const": "fixed_point", + "type": "string" + }, + "micros": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "kind", + "micros" + ], + "type": "object" + }, + { + "properties": { + "kind": { + "const": "interval", + "type": "string" + }, + "lower_micros": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "upper_micros": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "kind", + "lower_micros", + "upper_micros" + ], + "type": "object" + } + ] + }, "ExecutionBlockedCauseV1": { "description": "Cause a work item was blocked for.", "enum": [ @@ -14461,6 +15071,15 @@ "description": "Strongly typed canonical identity: `FileOccurrenceId`.", "type": "string" }, + "FreshnessState": { + "description": "Application-level freshness. Missing or partial truth never becomes current.", + "enum": [ + "current", + "stale", + "unknown" + ], + "type": "string" + }, "GenerateProposalRequest": { "additionalProperties": false, "description": "Read-only proposal generation over one exact current product graph.", @@ -20495,6 +21114,225 @@ ], "type": "object" }, + "Omission": { + "additionalProperties": false, + "properties": { + "count": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "domain": { + "$ref": "#/$defs/EvidenceDomain" + }, + "reason": { + "$ref": "#/$defs/OmissionReason" + } + }, + "required": [ + "domain", + "count", + "reason" + ], + "type": "object" + }, + "OmissionReason": { + "description": "Safe reason why authorized requested evidence was omitted.", + "oneOf": [ + { + "enum": [ + "budget", + "redacted", + "unavailable", + "unsupported", + "stale", + "failed", + "cancelled", + "timed_out", + "conflict" + ], + "type": "string" + }, + { + "const": "macro_body_unparsed", + "description": "Evidence came from a macro body that was not expanded, so what the\nexpansion defines or calls is not covered.", + "type": "string" + }, + { + "const": "import_unmodeled", + "description": "A caller sits under a `use` whose binding the index could not model\n(a glob in a block or inline module), so whether it calls the target\nis not covered.", + "type": "string" + }, + { + "const": "cursor_expired", + "description": "The continuation cursor can no longer be redeemed (its lifetime ended\nor the snapshot it pages is gone); restart the request without it.", + "type": "string" + }, + { + "const": "cursor_foreign", + "description": "The continuation cursor was issued for another project, worktree, or\nref; redeem it where it was issued.", + "type": "string" + } + ] + }, + "OperationBudgetUsage": { + "additionalProperties": false, + "description": "Bounded work accounting supplied by an owning port or transaction.", + "properties": { + "bytes_consumed": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "elapsed_micros": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "units_consumed": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "units_consumed", + "bytes_consumed", + "elapsed_micros" + ], + "type": "object" + }, + "OperationReceipt": { + "additionalProperties": false, + "description": "Canonical operation evidence. An admitted failure remains represented here\nrather than being replaced by a transport exception.", + "properties": { + "budget": { + "$ref": "#/$defs/OperationBudgetUsage" + }, + "cancellation": { + "anyOf": [ + { + "$ref": "#/$defs/CancellationObservation" + }, + { + "type": "null" + } + ] + }, + "effective_deadline": { + "$ref": "#/$defs/Deadline" + }, + "ended_at": { + "$ref": "#/$defs/UtcMicros" + }, + "started_at": { + "$ref": "#/$defs/UtcMicros" + }, + "termination": { + "$ref": "#/$defs/OperationTermination" + } + }, + "required": [ + "started_at", + "ended_at", + "effective_deadline", + "cancellation", + "budget", + "termination" + ], + "type": "object" + }, + "OperationTermination": { + "description": "Terminal state after an operation has been admitted.", + "enum": [ + "completed", + "cancelled", + "timed_out", + "failed", + "unavailable", + "partial", + "effect_unknown" + ], + "type": "string" + }, + "PageCursor": { + "description": "Exact continuation authority for the enclosing evidence page: an opaque\ncursor bound to the operation and request that minted it.", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "cursor": { + "type": "string" + }, + "kind": { + "const": "opaque", + "type": "string" + } + }, + "required": [ + "kind", + "cursor" + ], + "type": "object" + } + ] + }, + "PageState": { + "additionalProperties": false, + "description": "Stable page state. General cursor bytes stay opaque until authorization is\nrevalidated; structural fact cursors retain their canonical ordering type.", + "properties": { + "cursor": { + "anyOf": [ + { + "$ref": "#/$defs/PageCursor" + }, + { + "type": "null" + } + ] + }, + "expires_at": { + "anyOf": [ + { + "$ref": "#/$defs/UtcMicros" + }, + { + "type": "null" + } + ] + }, + "returned": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "sort_contract_id": { + "$ref": "#/$defs/SortContractId" + }, + "sort_revision": { + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "total": { + "format": "uint64", + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "sort_contract_id", + "sort_revision", + "total", + "returned", + "cursor", + "expires_at" + ], + "type": "object" + }, "PauseWorkRunCommand": { "additionalProperties": false, "properties": { @@ -20675,6 +21513,33 @@ ], "type": "object" }, + "PolicyDecisionRef": { + "additionalProperties": false, + "description": "A policy decision pinned into a receipt or provider identity.", + "properties": { + "decision_id": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/ManifestDigest" + }, + "evaluator_revision": { + "$ref": "#/$defs/ComponentVersion" + }, + "revision": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "decision_id", + "revision", + "digest", + "evaluator_revision" + ], + "type": "object" + }, "PolicyIdentifierV1": { "description": "A bounded, canonical identifier owned by the policy input schema.\n\nIt represents immutable references only; it is never a path, display\nlabel, provider account, branch name, or native object identifier.", "type": "string" @@ -20774,6 +21639,41 @@ ], "type": "object" }, + "PreviewResult": { + "additionalProperties": false, + "description": "Read-only preview of a future typed effect.", + "properties": { + "authority": { + "$ref": "#/$defs/AuthorityReceipt" + }, + "effect_class": { + "$ref": "#/$defs/EffectClass" + }, + "execution": { + "$ref": "#/$defs/OperationReceipt" + }, + "expected_state": { + "$ref": "#/$defs/ManifestDigest" + }, + "payload": true, + "preview_digest": { + "$ref": "#/$defs/ManifestDigest" + }, + "preview_id": { + "type": "string" + } + }, + "required": [ + "preview_id", + "preview_digest", + "effect_class", + "authority", + "expected_state", + "execution", + "payload" + ], + "type": "object" + }, "PrimitiveFreshnessStateV1": { "description": "Whether the served code generation is known current at serve time.", "enum": [ @@ -21300,6 +22200,29 @@ ], "type": "object" }, + "ProjectSettingsPatchResponseV1": { + "properties": { + "application_outcome": { + "anyOf": [ + { + "$ref": "#/$defs/ApplicationOutcome" + }, + { + "type": "null" + } + ], + "description": "Exact application settlement returned by the configuration binding.\n`None` means the submitted patch was already the current state." + }, + "current": { + "$ref": "#/$defs/DashboardEnvelopeV130" + } + }, + "required": [ + "application_outcome", + "current" + ], + "type": "object" + }, "ProjectSettingsPayloadV1": { "properties": { "config": { @@ -21768,6 +22691,15 @@ ], "type": "string" }, + "ReconciliationState": { + "description": "Reconciliation state retained after an admitted effect.", + "enum": [ + "pending", + "reconciled", + "failed" + ], + "type": "string" + }, "RecordWorkHandoffRequestV1": { "additionalProperties": false, "properties": { @@ -22514,6 +23446,94 @@ "description": "Strongly typed canonical identity: `RetrievalAnchorId`.", "type": "string" }, + "RetrieverContribution": { + "additionalProperties": false, + "description": "One source-owned contribution to the packet fold.", + "properties": { + "contract": { + "$ref": "#/$defs/ResultContractRef" + }, + "coverage": { + "$ref": "#/$defs/EvidenceCoverage" + }, + "domain": { + "$ref": "#/$defs/EvidenceDomain" + }, + "elapsed_budget_class": { + "$ref": "#/$defs/BudgetClass" + }, + "evidence_authorities": { + "items": { + "type": "string" + }, + "type": "array" + }, + "omitted_count": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "producer_revision": { + "$ref": "#/$defs/ComponentVersion" + }, + "provenance_anchors": { + "items": { + "$ref": "#/$defs/RetrievalAnchorId" + }, + "type": "array" + }, + "retriever_id": { + "$ref": "#/$defs/RetrieverId" + }, + "returned_count": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "score_ids": { + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "$ref": "#/$defs/RetrieverContributionState" + } + }, + "required": [ + "retriever_id", + "contract", + "producer_revision", + "domain", + "state", + "coverage", + "returned_count", + "omitted_count", + "score_ids", + "provenance_anchors", + "evidence_authorities", + "elapsed_budget_class" + ], + "type": "object" + }, + "RetrieverContributionState": { + "description": "Terminal state reported by one retriever contribution.", + "enum": [ + "completed", + "partial", + "unavailable", + "unsupported", + "stale", + "failed", + "cancelled", + "timed_out" + ], + "type": "string" + }, + "RetrieverId": { + "description": "Stable, canonical catalog identity for `RetrieverId`.", + "type": "string" + }, "RetryDirective": { "description": "Safe adapter-independent retry instruction. Adapters preserve it verbatim.", "enum": [ @@ -24500,6 +25520,22 @@ ], "type": "object" }, + "SettingsValidationIssueV1": { + "description": "One field-level settings validation failure.", + "properties": { + "field": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "field", + "message" + ], + "type": "object" + }, "SharedProfileStoreLocatorV1": { "additionalProperties": false, "description": "Logical and verified physical identity of the shared Profile shard.\n\nBrain and profile IDs select the shard; the verified store locator binds its\nphysical store. Lease incarnations and authority epochs remain runtime fences.", @@ -24856,6 +25892,10 @@ ], "type": "string" }, + "SortContractId": { + "description": "Stable, canonical catalog identity for `SortContractId`.", + "type": "string" + }, "SourceSpan": { "additionalProperties": false, "description": "Byte range inside one sanitized source file. Mutable line numbers are\nnever part of identity.", @@ -26364,6 +27404,63 @@ } ] }, + "TemporalState": { + "additionalProperties": false, + "description": "Temporal provenance for a packet.", + "properties": { + "code_graph_freshness": { + "anyOf": [ + { + "$ref": "#/$defs/CodeGraphReadFreshnessV1" + }, + { + "type": "null" + } + ] + }, + "freshness": { + "$ref": "#/$defs/FreshnessState" + }, + "requested_at": { + "$ref": "#/$defs/UtcMicros" + }, + "requested_mode": { + "$ref": "#/$defs/TemporalModeV1" + }, + "resolved_at": { + "$ref": "#/$defs/UtcMicros" + }, + "source_generation": { + "anyOf": [ + { + "$ref": "#/$defs/CodeGenerationId" + }, + { + "type": "null" + } + ] + }, + "watermark_digest": { + "anyOf": [ + { + "$ref": "#/$defs/ManifestDigest" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "requested_mode", + "requested_at", + "resolved_at", + "source_generation", + "watermark_digest", + "freshness" + ], + "type": "object" + }, "TestMapMeasurementV1": { "properties": { "algorithm": { @@ -35946,6 +37043,12 @@ "settings_project_patch": { "$ref": "#/$defs/ProjectSettingsPatch" }, + "settings_project_patch_response": { + "$ref": "#/$defs/ProjectSettingsPatchResponseV1" + }, + "settings_refusal": { + "$ref": "#/$defs/DashboardConfigurationRefusalV1" + }, "settings_user_patch": { "$ref": "#/$defs/UserSettingsPatch" }, @@ -36198,6 +37301,8 @@ "code_index_freshness", "settings", "settings_project_patch", + "settings_project_patch_response", + "settings_refusal", "settings_user_patch", "settings_code_index_worker_patch", "observatory", diff --git a/dashboard/codegen/src/generate.ts b/dashboard/codegen/src/generate.ts index 1c3d4fc890..9524e0ca05 100644 --- a/dashboard/codegen/src/generate.ts +++ b/dashboard/codegen/src/generate.ts @@ -449,7 +449,7 @@ function emitNamedDef( function emitAlias(desc: string, name: string, ann: string, zod: string, ts: string): EmittedDef { return { typeName: name, - types: `export type ${name} = ${ts};`, + types: `export type ${name} =${ts.startsWith("\n") ? "" : " "}${ts};`, decoders: `${desc}export const ${name}Schema: ${ann} = ${zod};`, }; } diff --git a/dashboard/src/contracts/decoders.ts b/dashboard/src/contracts/decoders.ts index 67b2c136d4..a9d127776f 100644 --- a/dashboard/src/contracts/decoders.ts +++ b/dashboard/src/contracts/decoders.ts @@ -54,6 +54,7 @@ import type { AnalyticsUsageCategoryV1, AnalyticsUsageSummaryV1, ApplicationExecutionFailureClassV1, + ApplicationOutcome, ApplicationProblemDetailV1, ApplicationProblemEnvelope, ApplicationProblemKind, @@ -61,6 +62,7 @@ import type { ApplicationUnavailableClassV1, ApplyWorkRelationReplanRequestV1, AttemptId, + AuthorityReceipt, AuthorizedRoot, AuthorizedScopeSet, AuthorizedWorkProductScopeV1, @@ -104,8 +106,10 @@ import type { BranchNamingPolicyV1, BranchTopologyKindV1, BranchTopologyPolicyV1, + BudgetClass, CallChainMeasurementV1, CallChainStepV1, + CancellationObservation, CancellationStage, CancelWorkAttemptCommand, CanonicalGitRefNameV1, @@ -118,6 +122,7 @@ import type { CodeCloneIndexResourcesV1, CodeCloneIndexStatusV1, CodeGenerationId, + CodeGraphReadFreshnessV1, CodeGraphServingReadinessV1, CodeIndexBuildBlockedReasonV1, CodeIndexBuildPhaseV1, @@ -143,6 +148,7 @@ import type { ComparisonDispositionV1, ComplexityAnalysisV1, ComponentVersion, + ConfigurationRefusalV1, ConfigurationRevisionId, ConfigurationSnapshotId, ContentDigest, @@ -160,6 +166,7 @@ import type { CrossMergePolicyV1, DaemonServiceUnitObservationV1, DashboardAuthorizationV1, + DashboardConfigurationRefusalV1, DashboardCoverageCompletenessV1, DashboardCoverageV1, DashboardDomainStateV1, @@ -172,6 +179,7 @@ import type { DashboardTimeV1, DashboardVersionV1, DashboardWatermarkV1, + Deadline, DecideWorkProposalRequestV1, DecideWorkRelationReplanRequestV1, DecisionEngineSettingsV1, @@ -244,6 +252,7 @@ import type { DeliverySharedCodeRefStateV1, DeliverySharedCodeRefV1, DiagnosticsSearchedTsconfigV1, + DisclosureClass, DoctorCoverageCompletenessV1, DoctorCoverageStatementV1, DoctorEvidenceReferenceV1, @@ -262,11 +271,17 @@ import type { DuplicateEffortKindV1, EffectClass, EffectReceipt, + EffectResult, EffectTermination, EnvironmentSettingsPayloadV1, EnvironmentVariableV1, + EvidenceAuthority, EvidenceCoverage, EvidenceDomain, + EvidencePacket, + EvidenceScore, + EvidenceScoreKind, + EvidenceScoreValue, ExecutionBlockedCauseV1, ExecutionConcurrencyPhaseV1, ExecutionConflictKindV1, @@ -352,6 +367,7 @@ import type { FeedbackSystemMetricV1, FeedbackSystemQualityReadModelV1, FileOccurrenceId, + FreshnessState, GeneratedWorkProposal, GenerateProposalRequest, GitHubStackedPullRequestPolicyV1, @@ -516,16 +532,25 @@ import type { ObservabilityHorizonV1, ObservationSourceIdentityV1, ObservatoryReadModelV1, + Omission, + OmissionReason, + OperationBudgetUsage, + OperationReceipt, + OperationTermination, + PageCursor, + PageState, PauseWorkRunCommand, PayloadAccessState, PayloadDigestV1, PayloadReferenceV1, PerformanceComparisonReadModelV1, + PolicyDecisionRef, PolicyIdentifierV1, PrAutoTrackEntryV1, PrAutoTrackPayloadV1, PrepareWorkDuplicateAdjudicationRequestV1, PrepareWorkProductMutationRequestV1, + PreviewResult, PrimitiveFreshnessStateV1, PrimitiveIndexingStateV1, PrimitiveSearchFreshnessV1, @@ -543,6 +568,7 @@ import type { ProjectRegistrySummary, ProjectRepoGroup, ProjectSettingsPatch, + ProjectSettingsPatchResponseV1, ProjectSettingsPayloadV1, ProjectsPayloadV1, ProposalId, @@ -560,6 +586,7 @@ import type { ProximityWarningClassV1, PublicCodeProject, QuantityEvidenceClassV1, + ReconciliationState, RecordWorkHandoffRequestV1, RefId, RegisteredRootLocatorV1, @@ -592,6 +619,9 @@ import type { ResumeWorkAttemptsCommand, ResumeWorkRunCommand, RetrievalAnchorId, + RetrieverContribution, + RetrieverContributionState, + RetrieverId, RetryDirective, RetryScope, RetryWorkAttemptCommandV1, @@ -654,6 +684,7 @@ import type { SessionRetrievalStructuralRefusalV1, SettingsAvailabilityV1, SettingsPayloadV1, + SettingsValidationIssueV1, SharedProfileStoreLocatorV1, SignificantTableGrowthSampleV1, SimilarCoverageV1, @@ -664,6 +695,7 @@ import type { SkillInstallTarget, SkillOutcomeRecord, SkillOutcomeVerdict, + SortContractId, SourceSpan, SourceStoreId, StartWorkAttemptCommand, @@ -701,6 +733,7 @@ import type { TelemetrySettingsPatch, TelemetrySettingsV1, TemporalModeV1, + TemporalState, TestMapMeasurementV1, TokenActualV1, TokenPairV1, @@ -1506,6 +1539,21 @@ export const AnalyticsUsageSummaryV1Schema: z.ZodObject<{ /** Stable non-retryable class for an admitted execution failure. */ export const ApplicationExecutionFailureClassV1Schema: z.ZodEnum<{ "denied": "denied"; "malformed_output": "malformed_output"; "permanent": "permanent" }> = z.enum(["denied", "malformed_output", "permanent"]); +/** Canonical outcome family for an admitted application operation. */ +export const ApplicationOutcomeSchema: z.ZodType = z.discriminatedUnion("outcome", [z.object({ + outcome: z.literal("effect"), + value: z.lazy(() => EffectResultSchema), +}), z.object({ + outcome: z.literal("evidence"), + value: z.lazy(() => EvidencePacketSchema), +}), z.object({ + outcome: z.literal("preview"), + value: z.lazy(() => PreviewResultSchema), +}), z.object({ + outcome: z.literal("result"), + value: z.unknown(), +})]); + /** The structured facts behind a problem. Adapters read these fields; the problem's `message` is only their one human rendering. */ export const ApplicationProblemDetailV1Schema: z.ZodType = z.discriminatedUnion("kind", [z.object({ @@ -1635,6 +1683,25 @@ export const ApplyWorkRelationReplanRequestV1Schema: z.ZodObject<{ /** Strongly typed canonical identity: `AttemptId`. */ export const AttemptIdSchema: z.ZodType = z.string(); +/** Proof that this request crossed the current authorization boundary. */ +export const AuthorityReceiptSchema: z.ZodObject<{ + authorized_scope_digest: z.ZodType; + disclosure: z.ZodType; + grant_digest: z.ZodType; + grant_id: z.ZodType; + grant_revision: z.ZodType; + policy: z.ZodType; + revalidated_at: z.ZodType; +}, z.core.$strict> = z.object({ + authorized_scope_digest: z.lazy(() => ManifestDigestSchema), + disclosure: z.lazy(() => DisclosureClassSchema), + grant_digest: z.lazy(() => ManifestDigestSchema), + grant_id: z.string(), + grant_revision: z.number().int().min(0), + policy: z.lazy(() => PolicyDecisionRefSchema), + revalidated_at: z.lazy(() => UtcMicrosSchema), +}).strict(); + /** One exact application scope paired with its registered physical locator. The scope remains the identity authority. The locator is retained only so @@ -2201,6 +2268,9 @@ export const BranchTopologyPolicyV1Schema: z.ZodObject<{ allowed: z.array(z.lazy(() => BranchTopologyKindV1Schema)), }).strict(); +/** Bounded elapsed-work classification supplied by a retriever. */ +export const BudgetClassSchema: z.ZodEnum<{ "approaching_limit": "approaching_limit"; "exhausted": "exhausted"; "within_budget": "within_budget" }> = z.enum(["approaching_limit", "exhausted", "within_budget"]); + export const CallChainMeasurementV1Schema: z.ZodObject<{ complete: z.ZodType; directed: z.ZodType; @@ -2233,6 +2303,14 @@ export const CallChainStepV1Schema: z.ZodObject<{ node: z.lazy(() => NodeRefV1Schema), }); +export const CancellationObservationSchema: z.ZodObject<{ + observed_at: z.ZodType; + stage: z.ZodType; +}, z.core.$strict> = z.object({ + observed_at: z.lazy(() => UtcMicrosSchema), + stage: z.lazy(() => CancellationStageSchema), +}).strict(); + /** Exact stage at which cancellation or deadline state was observed. */ export const CancellationStageSchema: z.ZodEnum<{ "after_commit": "after_commit"; "before_admission": "before_admission"; "before_effect": "before_effect"; "before_read": "before_read"; "during_read": "during_read"; "effect_in_flight": "effect_in_flight"; "reconciling": "reconciling" }> = z.enum(["after_commit", "before_admission", "before_effect", "before_read", "during_read", "effect_in_flight", "reconciling"]); @@ -2358,6 +2436,15 @@ export const CodeCloneIndexStatusV1Schema: z.ZodType = z.string(); +/** Freshness of the generation a verified graph read serves. */ +export const CodeGraphReadFreshnessV1Schema: z.ZodType = z.discriminatedUnion("state", [z.object({ + state: z.literal("current"), +}), z.object({ + rebuild_in_flight: z.boolean(), + sealed_at: z.lazy(() => UtcMicrosSchema), + state: z.literal("last_complete_stale"), +})]); + /** Interactive graph-serving state for the latest sealed generation. A sealed generation can expose truthful census statistics before its graph @@ -2677,6 +2764,20 @@ export const ComplexityAnalysisV1Schema: z.ZodUnion<[z.ZodLiteral<"complete">, z /** Strongly typed canonical identity: `ComponentVersion`. */ export const ComponentVersionSchema: z.ZodType = z.string(); +/** Configuration refusals carrying the same stable codes as the HTTP routes. */ +export const ConfigurationRefusalV1Schema: z.ZodType = z.discriminatedUnion("code", [z.object({ + code: z.literal("application_contract_violation"), + detail: z.string(), +}).strict(), z.object({ + code: z.literal("configuration_authority_unavailable"), + detail: z.string(), +}).strict(), z.object({ + actual_revision_id: z.string(), + code: z.literal("configuration_revision_conflict"), + detail: z.string(), + expected_revision_id: z.string(), +}).strict()]); + /** Strongly typed canonical identity: `ConfigurationRevisionId`. */ export const ConfigurationRevisionIdSchema: z.ZodType = z.string(); @@ -2830,6 +2931,17 @@ export const DashboardAuthorizationV1Schema: z.ZodType; +} | ConfigurationRefusalV1 | ApplicationProblemEnvelope, unknown> = z.union([z.object({ + detail: z.string(), + validation_errors: z.array(z.lazy(() => SettingsValidationIssueV1Schema)), +}).strict(), z.lazy(() => ConfigurationRefusalV1Schema), z.lazy(() => ApplicationProblemEnvelopeSchema)]); + /** Coverage completeness axis. `Unsupported` distinguishes "the source that would establish coverage is not wired" from `Unknown` ("coverage could not be determined"). */ @@ -2968,6 +3080,13 @@ export const DashboardWatermarkV1Schema: z.ZodObject<{ watermark: z.string(), }); +/** One immutable deadline supplied by the caller or upstream admission layer. */ +export const DeadlineSchema: z.ZodObject<{ + expires_at: z.ZodType; +}, z.core.$strict> = z.object({ + expires_at: z.lazy(() => UtcMicrosSchema), +}).strict(); + export const DecideWorkProposalRequestV1Schema: z.ZodObject<{ disposition: z.ZodType; mutation: z.ZodType; @@ -3969,6 +4088,9 @@ export const DiagnosticsSearchedTsconfigV1Schema: z.ZodObject<{ present: z.boolean(), }).strict(); +/** Disclosure ceiling carried by an immutable grant and revalidated at sinks. */ +export const DisclosureClassSchema: z.ZodEnum<{ "evidence": "evidence"; "metadata": "metadata"; "sensitive": "sensitive" }> = z.enum(["evidence", "metadata", "sensitive"]); + /** Whether Doctor observed all of a family's evidence sources. */ export const DoctorCoverageCompletenessV1Schema: z.ZodUnion<[z.ZodLiteral<"complete">, z.ZodLiteral<"partial">, z.ZodLiteral<"unknown">]> = z.union([z.literal("complete"), z.literal("partial"), z.literal("unknown")]); @@ -4153,6 +4275,30 @@ export const EffectReceiptSchema: z.ZodObject<{ scope: z.lazy(() => ResolvedScopeSchema), }).strict(); +/** Result of an admitted effect. `EffectUnknown` remains a receipt state and +cannot be remapped into a pre-admission problem. */ +export const EffectResultSchema: z.ZodObject<{ + authority: z.ZodType; + effect_class: z.ZodType; + effect_id: z.ZodType; + execution: z.ZodType; + expected_state: z.ZodType; + idempotency_key: z.ZodType; + payload: z.ZodType; + receipt: z.ZodType; + reconciliation: z.ZodType; +}, z.core.$strict> = z.object({ + authority: z.lazy(() => AuthorityReceiptSchema), + effect_class: z.lazy(() => EffectClassSchema), + effect_id: z.string(), + execution: z.lazy(() => OperationReceiptSchema), + expected_state: z.lazy(() => ManifestDigestSchema), + idempotency_key: z.string(), + payload: z.unknown(), + receipt: z.lazy(() => EffectReceiptSchema), + reconciliation: z.lazy(() => ReconciliationStateSchema), +}).strict(); + /** Durable effect receipt terminal state. */ export const EffectTerminationSchema: z.ZodEnum<{ "cancelled": "cancelled"; "completed": "completed"; "effect_unknown": "effect_unknown"; "failed": "failed"; "partial": "partial"; "timed_out": "timed_out" }> = z.enum(["cancelled", "completed", "effect_unknown", "failed", "partial", "timed_out"]); @@ -4180,6 +4326,23 @@ export const EnvironmentVariableV1Schema: z.ZodObject<{ value: z.string().nullable(), }); +/** Evidence authority is separate from caller authority and cannot grant access. */ +export const EvidenceAuthoritySchema: z.ZodObject<{ + evidence_id: z.ZodType; + horizon: z.ZodType; + producer: z.ZodType; + revision: z.ZodType; + scope: z.ZodType; + source_kind: z.ZodType; +}, z.core.$strict> = z.object({ + evidence_id: z.string(), + horizon: z.union([z.lazy(() => UtcMicrosSchema), z.null()]), + producer: z.string(), + revision: z.lazy(() => ComponentVersionSchema), + scope: z.lazy(() => ResolvedScopeSchema), + source_kind: z.string(), +}).strict(); + /** Deterministic coverage fold input for an evidence packet. */ export const EvidenceCoverageSchema: z.ZodObject<{ completeness: z.ZodType; @@ -4200,6 +4363,62 @@ export const EvidenceCoverageSchema: z.ZodObject<{ /** Requested evidence domain for bounded coverage and omissions. */ export const EvidenceDomainSchema: z.ZodEnum<{ "anchor": "anchor"; "diagnostic": "diagnostic"; "graph": "graph"; "operational": "operational"; "source": "source"; "symbol": "symbol"; "temporal": "temporal"; "test": "test" }> = z.enum(["anchor", "diagnostic", "graph", "operational", "source", "symbol", "temporal", "test"]); +/** Immutable evidence packet consumed by adapters and later planner work. */ +export const EvidencePacketSchema: z.ZodObject<{ + authority: z.ZodType; + contributions: z.ZodType, unknown>; + coverage: z.ZodType; + evidence_authorities: z.ZodType, unknown>; + execution: z.ZodType; + omissions: z.ZodType, unknown>; + page: z.ZodType; + payload: z.ZodType; + scores: z.ZodType, unknown>; + temporal: z.ZodType; +}, z.core.$strict> = z.object({ + authority: z.lazy(() => AuthorityReceiptSchema), + contributions: z.array(z.lazy(() => RetrieverContributionSchema)), + coverage: z.lazy(() => EvidenceCoverageSchema), + evidence_authorities: z.array(z.lazy(() => EvidenceAuthoritySchema)), + execution: z.lazy(() => OperationReceiptSchema), + omissions: z.array(z.lazy(() => OmissionSchema)), + page: z.lazy(() => PageStateSchema), + payload: z.unknown(), + scores: z.array(z.lazy(() => EvidenceScoreSchema)), + temporal: z.lazy(() => TemporalStateSchema), +}).strict(); + +export const EvidenceScoreSchema: z.ZodObject<{ + calibration_revision: z.ZodType; + calibration_valid: z.ZodType; + deterministic_components: z.ZodType, unknown>; + kind: z.ZodType; + score_id: z.ZodType; + value: z.ZodType; +}, z.core.$strict> = z.object({ + calibration_revision: z.union([z.lazy(() => ComponentVersionSchema), z.null()]), + calibration_valid: z.boolean().nullable(), + deterministic_components: z.array(z.string()), + kind: z.lazy(() => EvidenceScoreKindSchema), + score_id: z.string(), + value: z.lazy(() => EvidenceScoreValueSchema), +}).strict(); + +/** Evidence score semantics. Scores are metadata, never authority. */ +export const EvidenceScoreKindSchema: z.ZodEnum<{ "calibrated_interval": "calibrated_interval"; "calibrated_probability": "calibrated_probability"; "heuristic_score": "heuristic_score"; "ordinal_rank": "ordinal_rank" }> = z.enum(["calibrated_interval", "calibrated_probability", "heuristic_score", "ordinal_rank"]); + +export const EvidenceScoreValueSchema: z.ZodType = z.discriminatedUnion("kind", [z.object({ + kind: z.literal("fixed_point"), + micros: z.number().int().min(0), +}), z.object({ + kind: z.literal("interval"), + lower_micros: z.number().int().min(0), + upper_micros: z.number().int().min(0), +}), z.object({ + kind: z.literal("ordinal"), + rank: z.number().int().min(0), +})]); + /** Cause a work item was blocked for. */ export const ExecutionBlockedCauseV1Schema: z.ZodEnum<{ "backpressure": "backpressure"; "capability": "capability"; "ci": "ci"; "conflict": "conflict"; "dependency": "dependency"; "effect_unknown": "effect_unknown"; "lease": "lease"; "needs_input": "needs_input"; "other": "other"; "policy": "policy"; "review": "review"; "scope": "scope"; "test": "test"; "unknown": "unknown" }> = z.enum(["backpressure", "capability", "ci", "conflict", "dependency", "effect_unknown", "lease", "needs_input", "other", "policy", "review", "scope", "test", "unknown"]); @@ -5053,6 +5272,9 @@ export const FeedbackSystemQualityReadModelV1Schema: z.ZodObject<{ /** Strongly typed canonical identity: `FileOccurrenceId`. */ export const FileOccurrenceIdSchema: z.ZodType = z.string(); +/** Application-level freshness. Missing or partial truth never becomes current. */ +export const FreshnessStateSchema: z.ZodEnum<{ "current": "current"; "stale": "stale"; "unknown": "unknown" }> = z.enum(["current", "stale", "unknown"]); + /** A canonical product proposal and the exact verified graph that licensed it. `proposal` can be moved directly into a `DecideWorkProposalRequestV1`; @@ -7273,6 +7495,76 @@ export const ObservatoryReadModelV1Schema: z.ZodObject<{ watermark: z.string(), }); +export const OmissionSchema: z.ZodObject<{ + count: z.ZodType; + domain: z.ZodType; + reason: z.ZodType; +}, z.core.$strict> = z.object({ + count: z.number().int().min(0), + domain: z.lazy(() => EvidenceDomainSchema), + reason: z.lazy(() => OmissionReasonSchema), +}).strict(); + +/** Safe reason why authorized requested evidence was omitted. */ +export const OmissionReasonSchema: z.ZodType<"budget" | "cancelled" | "conflict" | "failed" | "redacted" | "stale" | "timed_out" | "unavailable" | "unsupported" | "macro_body_unparsed" | "import_unmodeled" | "cursor_expired" | "cursor_foreign", unknown> = z.union([z.enum(["budget", "cancelled", "conflict", "failed", "redacted", "stale", "timed_out", "unavailable", "unsupported"]), z.literal("macro_body_unparsed"), z.literal("import_unmodeled"), z.literal("cursor_expired"), z.literal("cursor_foreign")]); + +/** Bounded work accounting supplied by an owning port or transaction. */ +export const OperationBudgetUsageSchema: z.ZodObject<{ + bytes_consumed: z.ZodType; + elapsed_micros: z.ZodType; + units_consumed: z.ZodType; +}, z.core.$strict> = z.object({ + bytes_consumed: z.number().int().min(0), + elapsed_micros: z.number().int().min(0), + units_consumed: z.number().int().min(0), +}).strict(); + +/** Canonical operation evidence. An admitted failure remains represented here +rather than being replaced by a transport exception. */ +export const OperationReceiptSchema: z.ZodObject<{ + budget: z.ZodType; + cancellation: z.ZodType; + effective_deadline: z.ZodType; + ended_at: z.ZodType; + started_at: z.ZodType; + termination: z.ZodType; +}, z.core.$strict> = z.object({ + budget: z.lazy(() => OperationBudgetUsageSchema), + cancellation: z.union([z.lazy(() => CancellationObservationSchema), z.null()]), + effective_deadline: z.lazy(() => DeadlineSchema), + ended_at: z.lazy(() => UtcMicrosSchema), + started_at: z.lazy(() => UtcMicrosSchema), + termination: z.lazy(() => OperationTerminationSchema), +}).strict(); + +/** Terminal state after an operation has been admitted. */ +export const OperationTerminationSchema: z.ZodEnum<{ "cancelled": "cancelled"; "completed": "completed"; "effect_unknown": "effect_unknown"; "failed": "failed"; "partial": "partial"; "timed_out": "timed_out"; "unavailable": "unavailable" }> = z.enum(["cancelled", "completed", "effect_unknown", "failed", "partial", "timed_out", "unavailable"]); + +/** Exact continuation authority for the enclosing evidence page: an opaque +cursor bound to the operation and request that minted it. */ +export const PageCursorSchema: z.ZodType = z.discriminatedUnion("kind", [z.object({ + cursor: z.string(), + kind: z.literal("opaque"), +}).strict()]); + +/** Stable page state. General cursor bytes stay opaque until authorization is +revalidated; structural fact cursors retain their canonical ordering type. */ +export const PageStateSchema: z.ZodObject<{ + cursor: z.ZodType; + expires_at: z.ZodType; + returned: z.ZodType; + sort_contract_id: z.ZodType; + sort_revision: z.ZodType; + total: z.ZodType; +}, z.core.$strict> = z.object({ + cursor: z.union([z.lazy(() => PageCursorSchema), z.null()]), + expires_at: z.union([z.lazy(() => UtcMicrosSchema), z.null()]), + returned: z.number().int().min(0), + sort_contract_id: z.lazy(() => SortContractIdSchema), + sort_revision: z.number().refine(Number.isInteger).min(0), + total: z.number().int().min(0).nullable(), +}).strict(); + export const PauseWorkRunCommandSchema: z.ZodObject<{ expected_authority_version: z.ZodType; occurred_at: z.ZodType; @@ -7334,6 +7626,19 @@ export const PerformanceComparisonReadModelV1Schema: z.ZodObject<{ workload: z.string().nullable(), }); +/** A policy decision pinned into a receipt or provider identity. */ +export const PolicyDecisionRefSchema: z.ZodObject<{ + decision_id: z.ZodType; + digest: z.ZodType; + evaluator_revision: z.ZodType; + revision: z.ZodType; +}, z.core.$strict> = z.object({ + decision_id: z.string(), + digest: z.lazy(() => ManifestDigestSchema), + evaluator_revision: z.lazy(() => ComponentVersionSchema), + revision: z.number().int().min(0), +}).strict(); + /** A bounded, canonical identifier owned by the policy input schema. It represents immutable references only; it is never a path, display @@ -7386,6 +7691,25 @@ export const PrepareWorkProductMutationRequestV1Schema: z.ZodObject<{ selection: z.lazy(() => WorkProductSelectionScopeV1Schema), }).strict(); +/** Read-only preview of a future typed effect. */ +export const PreviewResultSchema: z.ZodObject<{ + authority: z.ZodType; + effect_class: z.ZodType; + execution: z.ZodType; + expected_state: z.ZodType; + payload: z.ZodType; + preview_digest: z.ZodType; + preview_id: z.ZodType; +}, z.core.$strict> = z.object({ + authority: z.lazy(() => AuthorityReceiptSchema), + effect_class: z.lazy(() => EffectClassSchema), + execution: z.lazy(() => OperationReceiptSchema), + expected_state: z.lazy(() => ManifestDigestSchema), + payload: z.unknown(), + preview_digest: z.lazy(() => ManifestDigestSchema), + preview_id: z.string(), +}).strict(); + /** Whether the served code generation is known current at serve time. */ export const PrimitiveFreshnessStateV1Schema: z.ZodEnum<{ "fresh": "fresh"; "possibly_stale": "possibly_stale" }> = z.enum(["fresh", "possibly_stale"]); @@ -7591,6 +7915,14 @@ export const ProjectSettingsPatchSchema: z.ZodObject<{ track_call_sites: z.boolean().nullable().optional(), }).strict(); +export const ProjectSettingsPatchResponseV1Schema: z.ZodObject<{ + application_outcome: z.ZodType; + current: z.ZodType, unknown>; +}> = z.object({ + application_outcome: z.union([z.lazy(() => ApplicationOutcomeSchema), z.null()]), + current: z.lazy(() => DashboardEnvelopeV1Schema(z.lazy(() => SettingsPayloadV1Schema))), +}); + export const ProjectSettingsPayloadV1Schema: z.ZodObject<{ config: z.ZodType; configuration_revision_id: z.ZodType; @@ -7761,6 +8093,9 @@ export const PublicCodeProjectSchema: z.ZodObject<{ export const QuantityEvidenceClassV1Schema: z.ZodEnum<{ "estimated": "estimated"; "locally_measured": "locally_measured"; "owner_receipt": "owner_receipt"; "unknown": "unknown" }> = z.enum(["estimated", "locally_measured", "owner_receipt", "unknown"]); +/** Reconciliation state retained after an admitted effect. */ +export const ReconciliationStateSchema: z.ZodEnum<{ "failed": "failed"; "pending": "pending"; "reconciled": "reconciled" }> = z.enum(["failed", "pending", "reconciled"]); + export const RecordWorkHandoffRequestV1Schema: z.ZodObject<{ handoff: z.ZodType; mutation: z.ZodType; @@ -8041,6 +8376,41 @@ export const ResumeWorkRunCommandSchema: z.ZodObject<{ /** Strongly typed canonical identity: `RetrievalAnchorId`. */ export const RetrievalAnchorIdSchema: z.ZodType = z.string(); +/** One source-owned contribution to the packet fold. */ +export const RetrieverContributionSchema: z.ZodObject<{ + contract: z.ZodType; + coverage: z.ZodType; + domain: z.ZodType; + elapsed_budget_class: z.ZodType; + evidence_authorities: z.ZodType, unknown>; + omitted_count: z.ZodType; + producer_revision: z.ZodType; + provenance_anchors: z.ZodType, unknown>; + retriever_id: z.ZodType; + returned_count: z.ZodType; + score_ids: z.ZodType, unknown>; + state: z.ZodType; +}, z.core.$strict> = z.object({ + contract: z.lazy(() => ResultContractRefSchema), + coverage: z.lazy(() => EvidenceCoverageSchema), + domain: z.lazy(() => EvidenceDomainSchema), + elapsed_budget_class: z.lazy(() => BudgetClassSchema), + evidence_authorities: z.array(z.string()), + omitted_count: z.number().int().min(0), + producer_revision: z.lazy(() => ComponentVersionSchema), + provenance_anchors: z.array(z.lazy(() => RetrievalAnchorIdSchema)), + retriever_id: z.lazy(() => RetrieverIdSchema), + returned_count: z.number().int().min(0), + score_ids: z.array(z.string()), + state: z.lazy(() => RetrieverContributionStateSchema), +}).strict(); + +/** Terminal state reported by one retriever contribution. */ +export const RetrieverContributionStateSchema: z.ZodEnum<{ "cancelled": "cancelled"; "completed": "completed"; "failed": "failed"; "partial": "partial"; "stale": "stale"; "timed_out": "timed_out"; "unavailable": "unavailable"; "unsupported": "unsupported" }> = z.enum(["cancelled", "completed", "failed", "partial", "stale", "timed_out", "unavailable", "unsupported"]); + +/** Stable, canonical catalog identity for `RetrieverId`. */ +export const RetrieverIdSchema: z.ZodType = z.string(); + /** Safe adapter-independent retry instruction. Adapters preserve it verbatim. */ export const RetryDirectiveSchema: z.ZodEnum<{ "after_delay": "after_delay"; "after_reconcile": "after_reconcile"; "after_revalidate": "after_revalidate"; "never": "never"; "same_request": "same_request" }> = z.enum(["after_delay", "after_reconcile", "after_revalidate", "never", "same_request"]); @@ -8761,6 +9131,15 @@ export const SettingsPayloadV1Schema: z.ZodObject<{ version: z.lazy(() => VersionSettingsPayloadV1Schema), }); +/** One field-level settings validation failure. */ +export const SettingsValidationIssueV1Schema: z.ZodObject<{ + field: z.ZodType; + message: z.ZodType; +}> = z.object({ + field: z.string(), + message: z.string(), +}); + /** Logical and verified physical identity of the shared Profile shard. Brain and profile IDs select the shard; the verified store locator binds its @@ -8890,6 +9269,9 @@ exists, so it is never a failure. Snapshots written before that distinction stored `unknown` as `ignored`. */ export const SkillOutcomeVerdictSchema: z.ZodEnum<{ "adopted": "adopted"; "too_early": "too_early"; "unknown": "unknown" }> = z.enum(["adopted", "too_early", "unknown"]); +/** Stable, canonical catalog identity for `SortContractId`. */ +export const SortContractIdSchema: z.ZodType = z.string(); + /** Byte range inside one sanitized source file. Mutable line numbers are never part of identity. */ export const SourceSpanSchema: z.ZodObject<{ @@ -9374,6 +9756,25 @@ export const TemporalModeV1Schema: z.ZodType = z.discri kind: z.literal("forensic"), }).strict()]); +/** Temporal provenance for a packet. */ +export const TemporalStateSchema: z.ZodObject<{ + code_graph_freshness: z.ZodOptional>; + freshness: z.ZodType; + requested_at: z.ZodType; + requested_mode: z.ZodType; + resolved_at: z.ZodType; + source_generation: z.ZodType; + watermark_digest: z.ZodType; +}, z.core.$strict> = z.object({ + code_graph_freshness: z.union([z.lazy(() => CodeGraphReadFreshnessV1Schema), z.null()]).optional(), + freshness: z.lazy(() => FreshnessStateSchema), + requested_at: z.lazy(() => UtcMicrosSchema), + requested_mode: z.lazy(() => TemporalModeV1Schema), + resolved_at: z.lazy(() => UtcMicrosSchema), + source_generation: z.union([z.lazy(() => CodeGenerationIdSchema), z.null()]), + watermark_digest: z.union([z.lazy(() => ManifestDigestSchema), z.null()]), +}).strict(); + export const TestMapMeasurementV1Schema: z.ZodObject<{ algorithm: z.ZodType; applicable: z.ZodType; diff --git a/dashboard/src/contracts/types.ts b/dashboard/src/contracts/types.ts index 7eceeb6733..5f38d42963 100644 --- a/dashboard/src/contracts/types.ts +++ b/dashboard/src/contracts/types.ts @@ -320,7 +320,25 @@ export type AnalyticsUsageSummaryV1 = { export type ApplicationExecutionFailureClassV1 = "denied" | "malformed_output" | "permanent"; -export type ApplicationProblemDetailV1 = +export type ApplicationOutcome = + | { + outcome: "effect"; + value: EffectResult; + } + | { + outcome: "evidence"; + value: EvidencePacket; + } + | { + outcome: "preview"; + value: PreviewResult; + } + | { + outcome: "result"; + value: unknown; + }; + +export type ApplicationProblemDetailV1 = | { kind: "code_graph_rewarming"; retry_after_millis: number; @@ -420,6 +438,16 @@ export type ApplyWorkRelationReplanRequestV1 = { export type AttemptId = string; +export type AuthorityReceipt = { + authorized_scope_digest: ManifestDigest; + disclosure: DisclosureClass; + grant_digest: ManifestDigest; + grant_id: string; + grant_revision: number; + policy: PolicyDecisionRef; + revalidated_at: UtcMicros; +}; + export type AuthorizedRoot = { locator: RegisteredRootLocatorV1 | null; scope: ResolvedScope; @@ -461,7 +489,7 @@ export type AutomaticFactReceiptsPayloadV1 = { export type AutomaticFactState = "applied" | "quarantined"; -export type AutomaticWorktreeGcV1 = +export type AutomaticWorktreeGcV1 = | { kind: "disabled"; } @@ -471,7 +499,7 @@ export type AutomaticWorktreeGcV1 = minimum_idle_seconds: number; }; -export type AutomationCommittedReceiptV1 = +export type AutomationCommittedReceiptV1 = | { kind: "automatic_fact"; receipt: MemoryAutomationFactReceiptV1; @@ -661,7 +689,7 @@ export type AutomationRunSummaryV1 = { skipped_count: number; }; -export type AutomationRunTerminalV1 = +export type AutomationRunTerminalV1 = | { status: "completed"; summary: AutomationRunSummaryV1; @@ -715,7 +743,7 @@ export type AutomationTrigger = "application" | "dashboard" | "host_receipt" | " export type BrainId = string; -export type BranchCollisionPolicyV1 = +export type BranchCollisionPolicyV1 = | { kind: "append_monotonic_ordinal"; maximum_attempts: number; @@ -724,7 +752,7 @@ export type BranchCollisionPolicyV1 = kind: "reject"; }; -export type BranchNameComponentV1 = +export type BranchNameComponentV1 = | { kind: "monotonic_collision_ordinal"; } @@ -755,6 +783,8 @@ export type BranchTopologyPolicyV1 = { allowed: Array; }; +export type BudgetClass = "approaching_limit" | "exhausted" | "within_budget"; + export type CallChainMeasurementV1 = { complete: boolean; directed: boolean; @@ -773,6 +803,11 @@ export type CallChainStepV1 = { node: NodeRefV1; }; +export type CancellationObservation = { + observed_at: UtcMicros; + stage: CancellationStage; +}; + export type CancellationStage = "after_commit" | "before_admission" | "before_effect" | "before_read" | "during_read" | "effect_in_flight" | "reconciling"; export type CancelWorkAttemptCommand = { @@ -828,7 +863,7 @@ export type CodeCloneIndexResourcesV1 = { stale_invalidations: number | null; }; -export type CodeCloneIndexStatusV1 = +export type CodeCloneIndexStatusV1 = | { observation: CodeCloneIndexObservationV1; omission_reasons: Array; @@ -850,7 +885,17 @@ export type CodeCloneIndexStatusV1 = export type CodeGenerationId = string; -export type CodeGraphServingReadinessV1 = +export type CodeGraphReadFreshnessV1 = + | { + state: "current"; + } + | { + rebuild_in_flight: boolean; + sealed_at: UtcMicros; + state: "last_complete_stale"; + }; + +export type CodeGraphServingReadinessV1 = | { state: "pending"; } @@ -955,7 +1000,7 @@ export type CodeIndexRouteRefusalV1 = { remediation: string; }; -export type CodeIndexSourceOmissionReasonV1 = +export type CodeIndexSourceOmissionReasonV1 = | { detail: string; kind: "privacy_withheld"; @@ -968,7 +1013,7 @@ export type CodeIndexStalenessStateV1 = "fresh" | "indexing" | "parked" | "refre export type CodeIndexWorkerLimitingReasonV1 = "automatic_all_cores" | "automatic_half_cores" | "configured_exact" | "environment_override" | "resident_memory"; -export type CodeIndexWorkerSelectionV1 = +export type CodeIndexWorkerSelectionV1 = | { mode: "automatic"; } @@ -1023,6 +1068,22 @@ export type ComplexityAnalysisV1 = "complete" | "traversal_budget_exhausted"; export type ComponentVersion = string; +export type ConfigurationRefusalV1 = + | { + code: "application_contract_violation"; + detail: string; + } + | { + code: "configuration_authority_unavailable"; + detail: string; + } + | { + actual_revision_id: string; + code: "configuration_revision_conflict"; + detail: string; + expected_revision_id: string; + }; + export type ConfigurationRevisionId = string; export type ConfigurationSnapshotId = string; @@ -1096,7 +1157,7 @@ export type CrossMergePolicyV1 = { default_mode: CrossMergeModeV1; }; -export type DaemonServiceUnitObservationV1 = +export type DaemonServiceUnitObservationV1 = | { path: string; serves: string; @@ -1110,7 +1171,7 @@ export type DaemonServiceUnitObservationV1 = state: "unobservable"; }; -export type DashboardAuthorizationV1 = +export type DashboardAuthorizationV1 = | { outcome: "authorized"; } @@ -1124,6 +1185,11 @@ export type DashboardAuthorizationV1 = outcome: "unauthorized"; }; +export type DashboardConfigurationRefusalV1 = { + detail: string; + validation_errors: Array; +} | ConfigurationRefusalV1 | ApplicationProblemEnvelope; + export type DashboardCoverageCompletenessV1 = "complete" | "partial" | "unknown" | "unsupported"; export type DashboardCoverageV1 = { @@ -1193,6 +1259,10 @@ export type DashboardWatermarkV1 = { watermark: string; }; +export type Deadline = { + expires_at: UtcMicros; +}; + export type DecideWorkProposalRequestV1 = { disposition: WorkProposalDispositionV12; mutation: WorkProductMutationIdentityV1; @@ -1233,7 +1303,7 @@ export type DeliveryAgentUsageV1 = { usage_coverage: ProviderUsageCoverageV1; }; -export type DeliveryAttentionEvidenceV1 = +export type DeliveryAttentionEvidenceV1 = | { failure_anchor: string; kind: "ci_failure"; @@ -1358,7 +1428,7 @@ export type DeliveryGenerationFreshnessV1 = { indexed_commit: string; }; -export type DeliveryGitHeadV1 = +export type DeliveryGitHeadV1 = | { branch: string; commit: string; @@ -1449,7 +1519,7 @@ export type DeliveryInboxV1 = { registry_state: DeliveryRegistryStateV1; }; -export type DeliveryMembershipBasisV1 = +export type DeliveryMembershipBasisV1 = | { branch_ref: string; head_commit_id: string; @@ -1492,7 +1562,7 @@ export type DeliveryOverviewV1 = { review_comments: DeliveryProjectionV14; }; -export type DeliveryProjectionV1 = +export type DeliveryProjectionV1 = | { state: "denied"; value: DeliveryGitStatusV1 | null; @@ -1535,7 +1605,7 @@ export type DeliveryProjectionV1 = value: DeliveryGitStatusV1 | null; }; -export type DeliveryProjectionV12 = +export type DeliveryProjectionV12 = | { state: "denied"; value: DeliveryCommitTimelineV1 | null; @@ -1578,7 +1648,7 @@ export type DeliveryProjectionV12 = value: DeliveryCommitTimelineV1 | null; }; -export type DeliveryProjectionV13 = +export type DeliveryProjectionV13 = | { state: "denied"; value: DeliveryPullRequestTimelineV1 | null; @@ -1621,7 +1691,7 @@ export type DeliveryProjectionV13 = value: DeliveryPullRequestTimelineV1 | null; }; -export type DeliveryProjectionV14 = +export type DeliveryProjectionV14 = | { state: "denied"; value: DeliveryReviewTimelineV1 | null; @@ -1664,7 +1734,7 @@ export type DeliveryProjectionV14 = value: DeliveryReviewTimelineV1 | null; }; -export type DeliveryProjectionV15 = +export type DeliveryProjectionV15 = | { state: "denied"; value: DeliveryCiTimelineV1 | null; @@ -1707,7 +1777,7 @@ export type DeliveryProjectionV15 = value: DeliveryCiTimelineV1 | null; }; -export type DeliveryProjectionV16 = +export type DeliveryProjectionV16 = | { state: "denied"; value: DeliveryFailureLocalizationTimelineV1 | null; @@ -1750,7 +1820,7 @@ export type DeliveryProjectionV16 = value: DeliveryFailureLocalizationTimelineV1 | null; }; -export type DeliveryProjectionV17 = +export type DeliveryProjectionV17 = | { state: "denied"; value: DeliveryReleaseTimelineV1 | null; @@ -1793,7 +1863,7 @@ export type DeliveryProjectionV17 = value: DeliveryReleaseTimelineV1 | null; }; -export type DeliveryProjectionV18 = +export type DeliveryProjectionV18 = | { state: "denied"; value: DeliveryGenerationFreshnessV1 | null; @@ -1836,7 +1906,7 @@ export type DeliveryProjectionV18 = value: DeliveryGenerationFreshnessV1 | null; }; -export type DeliveryProjectionV19 = +export type DeliveryProjectionV19 = | { state: "denied"; value: DeliveryAgentUsageV1 | null; @@ -2024,6 +2094,8 @@ export type DiagnosticsSearchedTsconfigV1 = { present: boolean; }; +export type DisclosureClass = "evidence" | "metadata" | "sensitive"; + export type DoctorCoverageCompletenessV1 = "complete" | "partial" | "unknown"; export type DoctorCoverageStatementV1 = { @@ -2040,7 +2112,7 @@ export type DoctorEvidenceRefV1 = { export type DoctorEvidenceStateV1 = "unsupported" | "absent" | "stale" | "degraded" | "partial" | "unknown" | "denied" | "healthy_complete_coverage"; -export type DoctorFamilyConsultationV1 = +export type DoctorFamilyConsultationV1 = | { status: "consulted"; } @@ -2112,6 +2184,18 @@ export type EffectReceipt = { scope: ResolvedScope; }; +export type EffectResult = { + authority: AuthorityReceipt; + effect_class: EffectClass; + effect_id: string; + execution: OperationReceipt; + expected_state: ManifestDigest; + idempotency_key: string; + payload: unknown; + receipt: EffectReceipt; + reconciliation: ReconciliationState; +}; + export type EffectTermination = "cancelled" | "completed" | "effect_unknown" | "failed" | "partial" | "timed_out"; export type EnvironmentSettingsPayloadV1 = { @@ -2128,6 +2212,15 @@ export type EnvironmentVariableV1 = { value: string | null; }; +export type EvidenceAuthority = { + evidence_id: string; + horizon: UtcMicros | null; + producer: string; + revision: ComponentVersion; + scope: ResolvedScope; + source_kind: string; +}; + export type EvidenceCoverage = { completeness: CoverageCompleteness; domains: Array; @@ -2139,6 +2232,45 @@ export type EvidenceCoverage = { export type EvidenceDomain = "anchor" | "diagnostic" | "graph" | "operational" | "source" | "symbol" | "temporal" | "test"; +export type EvidencePacket = { + authority: AuthorityReceipt; + contributions: Array; + coverage: EvidenceCoverage; + evidence_authorities: Array; + execution: OperationReceipt; + omissions: Array; + page: PageState; + payload: unknown; + scores: Array; + temporal: TemporalState; +}; + +export type EvidenceScore = { + calibration_revision: ComponentVersion | null; + calibration_valid: boolean | null; + deterministic_components: Array; + kind: EvidenceScoreKind; + score_id: string; + value: EvidenceScoreValue; +}; + +export type EvidenceScoreKind = "calibrated_interval" | "calibrated_probability" | "heuristic_score" | "ordinal_rank"; + +export type EvidenceScoreValue = + | { + kind: "fixed_point"; + micros: number; + } + | { + kind: "interval"; + lower_micros: number; + upper_micros: number; + } + | { + kind: "ordinal"; + rank: number; + }; + export type ExecutionBlockedCauseV1 = "backpressure" | "capability" | "ci" | "conflict" | "dependency" | "effect_unknown" | "lease" | "needs_input" | "other" | "policy" | "review" | "scope" | "test" | "unknown"; export type ExecutionConcurrencyPhaseV1 = "accepted" | "active" | "admitted" | "requested" | "useful"; @@ -2189,7 +2321,7 @@ export type ExecutionStackDriftKindV1 = "base_advanced" | "head_advanced" | "mer export type ExecutionSurfaceFamilyV1 = "cli" | "dashboard" | "hook" | "lsp" | "mcp" | "other"; -export type ExecutionTopologyDimensionV1 = +export type ExecutionTopologyDimensionV1 = | { dimension: "blocked_cause"; value: ExecutionBlockedCauseV1; @@ -2306,7 +2438,7 @@ export type ExecutionTopologyMetricsV1 = { watermark: string; }; -export type ExecutionTopologyViewV1 = +export type ExecutionTopologyViewV1 = | { state: "absent"; } @@ -2421,7 +2553,7 @@ export type FactCategoryV1 = "code_area" | "decision" | "general" | "project" | export type FactCommitDispositionV1 = "committed" | "idempotent_replay"; -export type FactCommitOwnerV1 = +export type FactCommitOwnerV1 = | { kind: "profile"; } @@ -2482,7 +2614,7 @@ export type FactOutcomeRecord = { export type FactOutcomeVerdict = "deleted" | "never_recalled" | "quarantined" | "recalled" | "recalled_and_helpful" | "unavailable"; -export type FactSearchGraphCoverageV1 = +export type FactSearchGraphCoverageV1 = | { expanded_fact_count: number; kind: "complete"; @@ -2613,7 +2745,7 @@ export type FeedbackProximityReadRequestV1 = { observed_at: UtcMicros; }; -export type FeedbackProximityReadResultV1 = +export type FeedbackProximityReadResultV1 = | { page: FeedbackProximityReadPageV1; state: "complete"; @@ -2641,7 +2773,7 @@ export type FeedbackProximityReadResultV1 = state: "unavailable"; }; -export type FeedbackProximityRelationV1 = +export type FeedbackProximityRelationV1 = | { relation_kind: "code_neighborhood_candidate"; warning_class: ProximityWarningClassV1; @@ -2702,6 +2834,8 @@ export type FeedbackSystemQualityReadModelV1 = { export type FileOccurrenceId = string; +export type FreshnessState = "current" | "stale" | "unknown"; + export type GeneratedWorkProposal = { calibration: WorkCalibrationEvidenceV1; decision: WorkProposalDecisionV1; @@ -2855,7 +2989,7 @@ export type GraphTotalsV1 = { export type HandoffOpenKindV1 = "investigation" | "task"; -export type HandoffOpenTargetV1 = +export type HandoffOpenTargetV1 = | { finding_id: FeedbackFindingId; kind: "investigation"; @@ -2892,7 +3026,7 @@ export type IntentWindowSettingsV1 = { max_user_turns: number; }; -export type JobDelivery = +export type JobDelivery = | { mode: "file"; path?: string | null | undefined; @@ -3174,7 +3308,7 @@ export type LoomEditedFileV1 = { session_id: string; }; -export type LoomEventV1 = +export type LoomEventV1 = | { kind: "pull_request"; label?: string | null | undefined; @@ -3346,7 +3480,7 @@ export type MemoryAutomationCurationMergeV1 = { winner_fact_id: FactId; }; -export type MemoryAutomationCurationOperationEffectV1 = +export type MemoryAutomationCurationOperationEffectV1 = | { closest_fact_id: FactId | null; commit: FactCommitReceiptV1 | null; @@ -3439,7 +3573,7 @@ export type MemoryAutomationFactDedupeValidationV1 = { export type MemoryAutomationFactDispositionV1 = "already_applied" | "applied" | "quarantined"; -export type MemoryAutomationFactEffectV1 = +export type MemoryAutomationFactEffectV1 = | { assertion_id: FactAssertionId; event_id: FactEventId; @@ -3602,7 +3736,7 @@ export type MemoryGraphEdgeV1 = { target: string; }; -export type MemoryGraphNodeV1 = +export type MemoryGraphNodeV1 = | { assertion_id: FactAssertionId; fact_id: FactId; @@ -3909,7 +4043,7 @@ export type MetricValueV1 = { value: number | null; }; -export type MultiRootCapabilityV1 = +export type MultiRootCapabilityV1 = | { revision: ScopeSetRevision; root_count: number; @@ -3941,7 +4075,7 @@ export type MultiRootExecuteRequestV1 = { scope_set_revision: ScopeSetRevision; }; -export type MultiRootOperationV1 = +export type MultiRootOperationV1 = | { kind: "feedback"; request: unknown; @@ -4031,6 +4165,46 @@ export type ObservatoryReadModelV1 = { watermark: string; }; +export type Omission = { + count: number; + domain: EvidenceDomain; + reason: OmissionReason; +}; + +export type OmissionReason = "budget" | "cancelled" | "conflict" | "failed" | "redacted" | "stale" | "timed_out" | "unavailable" | "unsupported" | "macro_body_unparsed" | "import_unmodeled" | "cursor_expired" | "cursor_foreign"; + +export type OperationBudgetUsage = { + bytes_consumed: number; + elapsed_micros: number; + units_consumed: number; +}; + +export type OperationReceipt = { + budget: OperationBudgetUsage; + cancellation: CancellationObservation | null; + effective_deadline: Deadline; + ended_at: UtcMicros; + started_at: UtcMicros; + termination: OperationTermination; +}; + +export type OperationTermination = "cancelled" | "completed" | "effect_unknown" | "failed" | "partial" | "timed_out" | "unavailable"; + +export type PageCursor = + | { + cursor: string; + kind: "opaque"; + }; + +export type PageState = { + cursor: PageCursor | null; + expires_at: UtcMicros | null; + returned: number; + sort_contract_id: SortContractId; + sort_revision: number; + total: number | null; +}; + export type PauseWorkRunCommand = { expected_authority_version: number | null; occurred_at: UtcMicros; @@ -4066,6 +4240,13 @@ export type PerformanceComparisonReadModelV1 = { workload: string | null; }; +export type PolicyDecisionRef = { + decision_id: string; + digest: ManifestDigest; + evaluator_revision: ComponentVersion; + revision: number; +}; + export type PolicyIdentifierV1 = string; export type PrAutoTrackEntryV1 = { @@ -4093,6 +4274,16 @@ export type PrepareWorkProductMutationRequestV1 = { selection: WorkProductSelectionScopeV1; }; +export type PreviewResult = { + authority: AuthorityReceipt; + effect_class: EffectClass; + execution: OperationReceipt; + expected_state: ManifestDigest; + payload: unknown; + preview_digest: ManifestDigest; + preview_id: string; +}; + export type PrimitiveFreshnessStateV1 = "fresh" | "possibly_stale"; export type PrimitiveIndexingStateV1 = { @@ -4202,6 +4393,11 @@ export type ProjectSettingsPatch = { track_call_sites?: boolean | null | undefined; }; +export type ProjectSettingsPatchResponseV1 = { + application_outcome: ApplicationOutcome | null; + current: DashboardEnvelopeV1; +}; + export type ProjectSettingsPayloadV1 = { config: ProjectEditableSettingsV1; configuration_revision_id: string; @@ -4230,7 +4426,7 @@ export type ProtectedRefRuleV1 = { selector: ProtectedRefSelectorV1; }; -export type ProtectedRefSelectorV1 = +export type ProtectedRefSelectorV1 = | { kind: "exact"; value: CanonicalGitRefNameV1; @@ -4304,6 +4500,8 @@ export type PublicCodeProject = { export type QuantityEvidenceClassV1 = "estimated" | "locally_measured" | "owner_receipt" | "unknown"; +export type ReconciliationState = "failed" | "pending" | "reconciled"; + export type RecordWorkHandoffRequestV1 = { handoff: WorkHandoffV1; mutation: WorkProductMutationIdentityV1; @@ -4359,7 +4557,7 @@ export type ReleaseWorkPlacementCommand = { export type RemoteAuthorityMissingReasonV1 = "authority_authentication_failed" | "authority_unreachable" | "caller_authentication_failed" | "enrollment_expired" | "enrollment_revoked" | "fence_unverified" | "insufficient_capability" | "placement_unknown" | "protocol_incompatible" | "registry_unavailable" | "scope_mismatch" | "writer_authority_unpublished"; -export type RemoteAuthoritySummaryV1 = +export type RemoteAuthoritySummaryV1 = | { fence: RemoteFenceSummaryV1; state: "available"; @@ -4385,7 +4583,7 @@ export type RemoteFenceSummaryV1 = { export type RemoteListenerKindV1 = "degraded" | "disabled" | "serving"; -export type RemoteOperationalStatusPayloadV1 = +export type RemoteOperationalStatusPayloadV1 = | { authority: RemoteAuthoritySummaryV1; coverage: DoctorCoverageCompletenessV1; @@ -4417,7 +4615,7 @@ export type RemoteSpoolSummaryV1 = { export type RepositoryId = string; -export type RepositoryPlacementScopeV1 = +export type RepositoryPlacementScopeV1 = | { kind: "all_authorized"; } @@ -4480,6 +4678,25 @@ export type ResumeWorkRunCommand = { export type RetrievalAnchorId = string; +export type RetrieverContribution = { + contract: ResultContractRef; + coverage: EvidenceCoverage; + domain: EvidenceDomain; + elapsed_budget_class: BudgetClass; + evidence_authorities: Array; + omitted_count: number; + producer_revision: ComponentVersion; + provenance_anchors: Array; + retriever_id: RetrieverId; + returned_count: number; + score_ids: Array; + state: RetrieverContributionState; +}; + +export type RetrieverContributionState = "cancelled" | "completed" | "failed" | "partial" | "stale" | "timed_out" | "unavailable" | "unsupported"; + +export type RetrieverId = string; + export type RetryDirective = "after_delay" | "after_reconcile" | "after_revalidate" | "never" | "same_request"; export type RetryScope = "fresh_request" | "same_operation" | "same_request"; @@ -4491,7 +4708,7 @@ export type RetryWorkAttemptCommandV1 = { original_attempt: WorkAttemptIdentityV1; }; -export type ReviewRequirementV1 = +export type ReviewRequirementV1 = | { kind: "code_owner_and_independent_review"; } @@ -4777,7 +4994,7 @@ export type SchemaConvergenceFindingV1 = { store: string; }; -export type SchemaConvergenceProgressV1 = +export type SchemaConvergenceProgressV1 = | { done: number; remaining: number; @@ -4795,7 +5012,7 @@ export type SchemaConvergenceStateV1 = "completed" | "degraded" | "pending_schem export type SchemaId = string; -export type ScopeOutcome_for_Array_of_AnyValue = +export type ScopeOutcome_for_Array_of_AnyValue = | { outcome: "denied"; } @@ -4817,7 +5034,7 @@ export type ScopeOutcome_for_Array_of_AnyValue = }; }; -export type ScopeOutcome_for_Nullable_RootGenerationV1 = +export type ScopeOutcome_for_Nullable_RootGenerationV1 = | { outcome: "denied"; } @@ -4839,7 +5056,7 @@ export type ScopeOutcome_for_Nullable_RootGenerationV1 = }; }; -export type ScopeOutcome_for_Nullable_string = +export type ScopeOutcome_for_Nullable_string = | { outcome: "denied"; } @@ -4883,7 +5100,7 @@ export type SessionRetrievalBudgetAccountingV1 = { observed: SessionRetrievalBudgetObservationV1; }; -export type SessionRetrievalBudgetObservationV1 = +export type SessionRetrievalBudgetObservationV1 = | { observation: "consumed_with_more_available"; units: number; @@ -4895,7 +5112,7 @@ export type SessionRetrievalBudgetObservationV1 = export type SessionRetrievalBudgetStageV1 = "candidate_read_exhausted" | "context_bytes" | "context_tokens" | "cursor_manifest_limit" | "estimator_version_mismatch" | "execution_work_exhausted" | "hydration_bytes" | "kernel_result_limit" | "participant_manifest_canonical_bytes" | "participant_manifest_participants" | "record_read_exhausted" | "request_candidate_bytes" | "request_context_bytes" | "request_hydration_bytes" | "request_hydration_limit" | "request_record_bytes" | "request_result_limit"; -export type SessionRetrievalStructuralRefusalV1 = +export type SessionRetrievalStructuralRefusalV1 = | { accounting: SessionRetrievalBudgetAccountingV1 | null; refusal: "budget_exhausted"; @@ -4925,6 +5142,11 @@ export type SettingsPayloadV1 = { version: VersionSettingsPayloadV1; }; +export type SettingsValidationIssueV1 = { + field: string; + message: string; +}; + export type SharedProfileStoreLocatorV1 = { brain_id: BrainId; profile_id: UserProfileId; @@ -4940,7 +5162,7 @@ export type SignificantTableGrowthSampleV1 = { table: string; }; -export type SimilarCoverageV1 = +export type SimilarCoverageV1 = | { status: "complete"; } @@ -5006,6 +5228,8 @@ export type SkillOutcomeRecord = { export type SkillOutcomeVerdict = "adopted" | "too_early" | "unknown"; +export type SortContractId = string; + export type SourceSpan = { end_byte: number; start_byte: number; @@ -5059,7 +5283,7 @@ export type StorageTelemetryPayloadV1 = { table_growth_threshold: TableGrowthThresholdV1; }; -export type StorageTelemetryReadV1 = +export type StorageTelemetryReadV1 = | { kind: "denied"; store: StoreKeyV1; @@ -5083,7 +5307,7 @@ export type StorageTelemetryReadV1 = store: StoreKeyV1; }; -export type StoreBudgetDimensionV1 = +export type StoreBudgetDimensionV1 = | { evaluation: StoreBudgetEvaluationV1; reason: string; @@ -5100,7 +5324,7 @@ export type StoreBudgetDimensionV1 = state: "unset"; }; -export type StoreBudgetEvaluationV1 = +export type StoreBudgetEvaluationV1 = | { observed: StorageByteSizeV1; overage: StorageByteSizeV1; @@ -5113,7 +5337,7 @@ export type StoreBudgetEvaluationV1 = state: "within_budget"; }; -export type StoreGrowthDimensionV1 = +export type StoreGrowthDimensionV1 = | { reason: string; state: "unknown"; @@ -5184,7 +5408,7 @@ export type StrataScanV1 = { files_examined: number; }; -export type StructureReadV1 = +export type StructureReadV1 = | { code: string; detail: string; @@ -5201,7 +5425,7 @@ export type StructureReadV1 = status: "unmeasured"; }; -export type StructureReadV12 = +export type StructureReadV12 = | { code: string; detail: string; @@ -5218,7 +5442,7 @@ export type StructureReadV12 = status: "unmeasured"; }; -export type StructureReadV13 = +export type StructureReadV13 = | { code: string; detail: string; @@ -5235,7 +5459,7 @@ export type StructureReadV13 = status: "unmeasured"; }; -export type StructureReadV14 = +export type StructureReadV14 = | { code: string; detail: string; @@ -5252,7 +5476,7 @@ export type StructureReadV14 = status: "unmeasured"; }; -export type StructureReadV15 = +export type StructureReadV15 = | { code: string; detail: string; @@ -5281,7 +5505,7 @@ export type SyncSettingsV1 = { auto_track_pr_poll_secs: number; }; -export type TableGrowthDimensionV1 = +export type TableGrowthDimensionV1 = | { coverage: DashboardCoverageV1; observed_at: number; @@ -5312,7 +5536,7 @@ export type TableGrowthDimensionV1 = state: "unsupported"; }; -export type TableGrowthOmissionV1 = +export type TableGrowthOmissionV1 = | { current_bytes: number; kind: "baseline_pending"; @@ -5358,7 +5582,7 @@ export type TelemetrySettingsV1 = { timings: boolean; }; -export type TemporalModeV1 = +export type TemporalModeV1 = | { cutoff: UtcMicros; kind: "as_of"; @@ -5373,6 +5597,16 @@ export type TemporalModeV1 = kind: "forensic"; }; +export type TemporalState = { + code_graph_freshness?: CodeGraphReadFreshnessV1 | null | undefined; + freshness: FreshnessState; + requested_at: UtcMicros; + requested_mode: TemporalModeV1; + resolved_at: UtcMicros; + source_generation: CodeGenerationId | null; + watermark_digest: ManifestDigest | null; +}; + export type TestMapMeasurementV1 = { algorithm: string; applicable: boolean; @@ -5505,7 +5739,7 @@ export type WorkArtifactHydrationRequestV1 = { page_size: number; }; -export type WorkArtifactHydrationV1 = +export type WorkArtifactHydrationV1 = | { state: "absent"; } @@ -5542,7 +5776,7 @@ export type WorkAttemptEvidenceRecordV1 = { stdout: WorkAttemptStreamSummaryV1 | null; }; -export type WorkAttemptEvidenceStateV1 = +export type WorkAttemptEvidenceStateV1 = | { state: "pending"; } @@ -5557,7 +5791,7 @@ export type WorkAttemptIdentityV1 = { task_id: TaskId; }; -export type WorkAttemptListCoverageV1 = +export type WorkAttemptListCoverageV1 = | { coverage: "capped"; remaining: number; @@ -5576,7 +5810,7 @@ export type WorkAttemptListRequestV1 = { page_size: number; }; -export type WorkAttemptListV1 = +export type WorkAttemptListV1 = | { state: "absent"; } @@ -5600,7 +5834,7 @@ export type WorkAttemptProjectionBindingV1 = { source_watermark: Record; }; -export type WorkAttemptProviderOutcomeV1 = +export type WorkAttemptProviderOutcomeV1 = | { outcome: "cancelled"; } @@ -5735,7 +5969,7 @@ export type WorkCancellationRequestV1 = { requested_at: UtcMicros; }; -export type WorkCancellationStateV1 = +export type WorkCancellationStateV1 = | { state: "acknowledged"; value: WorkCancellationAcknowledgementV1; @@ -5781,7 +6015,7 @@ export type WorkDecompositionProposalV1 = { rationale: Array; }; -export type WorkDuplicateAdjudicationAppendOutcomeV1 = +export type WorkDuplicateAdjudicationAppendOutcomeV1 = | { outcome: "appended"; receipt: WorkDuplicateAdjudicationReceiptV1; @@ -5831,7 +6065,7 @@ export type WorkEffectStateV1 = "compound_non_repeatable" | "intercepted" | "obs export type WorkEgressPolicy = "allowlisted" | "deny"; -export type WorkEvidenceContinuationV1 = +export type WorkEvidenceContinuationV1 = | { cursor: string; kind: "anchor"; @@ -5851,7 +6085,7 @@ export type WorkEvidenceCoverageV1 = { state: WorkEvidenceCoverageStateV1; }; -export type WorkEvidenceExpansionSelectorV1 = +export type WorkEvidenceExpansionSelectorV1 = | { kind: "anchor"; link_id: string; @@ -5903,7 +6137,7 @@ export type WorkEvidenceRetrieveRequestV1 = { verified_version: VerifiedWorkGraphVersionV1; }; -export type WorkEvidenceSourceV1 = +export type WorkEvidenceSourceV1 = | { hydration: WorkAnchorHydrationV1; kind: "anchor"; @@ -5940,7 +6174,7 @@ export type WorkExecutionEnvelopeV1 = { worktree_root: string; }; -export type WorkExecutionHistoryV1 = +export type WorkExecutionHistoryV1 = | { state: "absent"; } @@ -5999,7 +6233,7 @@ export type WorkExecutionSpanV1 = { wall_micros: number | null; }; -export type WorkExecutionTimingCoverageV1 = +export type WorkExecutionTimingCoverageV1 = | { coverage: "complete"; } @@ -6018,7 +6252,7 @@ export type WorkExperienceCandidateV1 = { item: WorkItemV1; }; -export type WorkExperienceCoverageV1 = +export type WorkExperienceCoverageV1 = | { applicable: number; coverage: "complete"; @@ -6056,7 +6290,7 @@ export type WorkExperienceV1 = { verified_version: VerifiedWorkGraphVersionV1; }; -export type WorkExpertiseAuthorizationV1 = +export type WorkExpertiseAuthorizationV1 = | { availability: "available"; categories: Array; @@ -6086,7 +6320,7 @@ export type WorkExpertiseLegalActionV1 = "allow_requested_categories" | "grant_p export type WorkExpertiseUnavailableReasonV1 = "project_consent_disabled" | "project_consent_expired" | "project_consent_not_yet_effective" | "requested_category_not_allowed" | "user_consent_disabled" | "user_consent_expired" | "user_consent_not_yet_effective"; -export type WorkFallbackTopology = +export type WorkFallbackTopology = | { executable: WorkExecutableReference; kind: "codex_cli"; @@ -6169,7 +6403,7 @@ export type WorkflowFanOutChildPlanV1 = { task_id: TaskId; }; -export type WorkflowFanOutFailurePolicyV1 = +export type WorkflowFanOutFailurePolicyV1 = | { policy: "collect"; } @@ -6232,7 +6466,7 @@ export type WorkflowRunEvent = { sequence: number; }; -export type WorkflowRunEventKind = +export type WorkflowRunEventKind = | { definition: WorkflowDefinition; fan_out_plans: Array; @@ -6353,7 +6587,7 @@ export type WorkflowSynthesisDraft = { export type WorkFrontierComparisonV1 = "agree" | "disagree" | "incomparable"; -export type WorkGraphChangeV1 = +export type WorkGraphChangeV1 = | { based_on_version: number; identity: WorkAttemptIdentityV1; @@ -6416,7 +6650,7 @@ export type WorkGraphChangeV1 = proposal_id: ProposalId; }; -export type WorkGraphReadModeV1 = +export type WorkGraphReadModeV1 = | { mode: "as_of"; valid_at: UtcMicros; @@ -6442,7 +6676,7 @@ export type WorkGraphReadRequestV1 = { selection: WorkProductSelectionScopeV1; }; -export type WorkGraphReadV1 = +export type WorkGraphReadV1 = | { authorized_scope: AuthorizedWorkProductScopeV1; mode: "absent"; @@ -6473,7 +6707,7 @@ export type WorkGraphReadV1 = timeline: WorkGraphTimelineV1; }; -export type WorkGraphSelectionCoverageV1 = +export type WorkGraphSelectionCoverageV1 = | { coverage: "complete"; covered_events: number; @@ -6485,7 +6719,7 @@ export type WorkGraphSelectionCoverageV1 = first_excluded_sequence: number; }; -export type WorkGraphTimelineCoverageV1 = +export type WorkGraphTimelineCoverageV1 = | { coverage: "complete"; returned: number; @@ -6573,7 +6807,7 @@ export type WorkKanbanProjectionV1 = { graph_version: number; }; -export type WorkLeakAdjudicationOutcomeV1 = +export type WorkLeakAdjudicationOutcomeV1 = | { outcome: "appended"; receipt: WorkLeakAdjudicationReceiptV1; @@ -6653,7 +6887,7 @@ export type WorkPlacementPreflightV1 = { target: WorkPlacementTargetV1; }; -export type WorkPlacementReadingV1 = +export type WorkPlacementReadingV1 = | { state: "absent"; } @@ -6704,7 +6938,7 @@ export type WorkPriorOutcomeV1 = { export type WorkPriorTerminalV1 = "cancelled" | "failed" | "succeeded" | "timed_out"; -export type WorkProductAuthorizedRelationScopeV1 = +export type WorkProductAuthorizedRelationScopeV1 = | { kind: "project"; project_id: ProjectId; @@ -6715,7 +6949,7 @@ export type WorkProductAuthorizedRelationScopeV1 = repository_id: RepositoryId; }; -export type WorkProductChangeDraftV1 = +export type WorkProductChangeDraftV1 = | { change: "accept_task"; evidence_by_criterion: Record; @@ -6766,7 +7000,7 @@ export type WorkProductEventEvidenceV1 = { source_store_id: SourceStoreId; }; -export type WorkProductEventPayloadV1 = +export type WorkProductEventPayloadV1 = | { change: WorkGraphChangeV1; kind: "changed"; @@ -6796,7 +7030,7 @@ export type WorkProductEventV1 = { source_watermark: Record; }; -export type WorkProductExpectedAuthorityV1 = +export type WorkProductExpectedAuthorityV1 = | { authority: "no_prior_graph"; } @@ -6831,7 +7065,7 @@ export type WorkProductMutationReceiptV1 = { verified_graph_version: VerifiedWorkGraphVersionV1; }; -export type WorkProductMutationRequestV1 = +export type WorkProductMutationRequestV1 = | { mutation: "accept_task"; request: AcceptWorkTaskRequestV1; @@ -6889,7 +7123,7 @@ export type WorkProductProjectionBundleV1 = { workload: WorkWorkloadProjectionV1; }; -export type WorkProductRelationV1 = +export type WorkProductRelationV1 = | { identity: WorkAttemptIdentityV1; kind: "accepted_attempt"; @@ -6947,7 +7181,7 @@ export type WorkProductRevisionPinsV1 = { policy_revision_id: string; }; -export type WorkProductSelectionScopeV1 = +export type WorkProductSelectionScopeV1 = | { selection: "profile_owned_no_git"; } @@ -7070,7 +7304,7 @@ export type WorkRankedRouteV1 = { sensitive_data_fitness: WorkOrdinalBandV1; }; -export type WorkRecoveryStateV1 = +export type WorkRecoveryStateV1 = | { state: "fresh"; } @@ -7109,7 +7343,7 @@ export type WorkRelationReplanProposalV1 = { export type WorkRestartReasonV1 = "failure_observed" | "lease_lost" | "process_lost" | "provider_unavailable"; -export type WorkRetryAttemptOutcomeV1 = +export type WorkRetryAttemptOutcomeV1 = | { attempt: WorkAttemptV1; outcome: "created"; @@ -7141,7 +7375,7 @@ export type WorkRetryReceiptV1 = { export type WorkRetrySourceV1 = "runtime"; -export type WorkRouteDecisionV1 = +export type WorkRouteDecisionV1 = | { decision: "abstained"; reason: string; @@ -7170,7 +7404,7 @@ export type WorkRoutePlanV1 = { export type WorkRunControlAuthorityV1 = number; -export type WorkRunControlReadingV1 = +export type WorkRunControlReadingV1 = | { control: WorkRunControlV1; live_attempts: Array; @@ -7215,7 +7449,7 @@ export type WorkRuntimeAttemptProjectionV1 = { state: WorkAttemptStateV1; }; -export type WorkRuntimeProjectionCoverageV1 = +export type WorkRuntimeProjectionCoverageV1 = | { coverage: "complete"; } @@ -7262,7 +7496,7 @@ export type WorkSubtaskSketchV1 = { summary: string; }; -export type WorkSynthesisAttemptV1 = +export type WorkSynthesisAttemptV1 = | { attempt: WorkAttemptV1; draft: WorkflowSynthesisDraft; @@ -7289,7 +7523,7 @@ export type WorkSynthesisSourceEnvelopeV1 = { source: WorkAttemptIdentityV1; }; -export type WorkSynthesisSourceOutcomeV1 = +export type WorkSynthesisSourceOutcomeV1 = | { evidence: ManifestDigest; outcome: "cancelled"; @@ -7384,7 +7618,7 @@ export type WorkTaskShapeV1 = { kind: WorkTaskShapeKindV1; }; -export type WorkTerminalEvidenceV1 = +export type WorkTerminalEvidenceV1 = | { evidence_digest: ManifestDigest; observed_at: UtcMicros; @@ -7467,7 +7701,7 @@ export type WorktreeCleanlinessRequirementV1 = "allow_untracked_only_for_preflig export type WorktreeId = string; -export type WorktreePlacementModeV1 = +export type WorktreePlacementModeV1 = | { kind: "configured_root"; root_id: string; diff --git a/dashboard/src/test/fixtures/settings-patch.json b/dashboard/src/test/fixtures/settings-patch.json new file mode 100644 index 0000000000..6c5d8473a9 --- /dev/null +++ b/dashboard/src/test/fixtures/settings-patch.json @@ -0,0 +1,639 @@ +{ + "cancelled": { + "body": { + "contract": { + "schema_id": "schema.configuration.batch.result", + "schema_revision": 1 + }, + "problem": { + "cancellation_stage": "before_admission", + "code": "cancelled", + "committed_receipt": null, + "coverage": null, + "detail": null, + "details": [], + "diagnostic": null, + "execution_failure_classification": null, + "kind": "cancelled", + "legal_actions": [], + "message": "The request was cancelled before admission", + "owning_layer": "application", + "request_id": "request.configuration.settings-fixture", + "retry": "never", + "retry_after_millis": null, + "retry_scope": null, + "retryable": false, + "revision": 1, + "terminality": "pre_admission", + "trace_id": "request.configuration.settings-fixture", + "unavailable_classification": null + }, + "request_id": "request.configuration.settings-fixture" + }, + "status": 409 + }, + "changed": { + "body": { + "application_outcome": { + "outcome": "effect", + "value": { + "authority": { + "authorized_scope_digest": "sha256:cb520e3731e8dbab143f4fc32b885fc977d467f4648a6429bf0ed9bf0294873e", + "disclosure": "metadata", + "grant_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_id": "grant.settings", + "grant_revision": 1, + "policy": { + "decision_id": "policy.settings", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "evaluator_revision": "policy.settings.v1", + "revision": 1 + }, + "revalidated_at": 10 + }, + "effect_class": "administrative", + "effect_id": "effect.configuration.settings", + "execution": { + "budget": { + "bytes_consumed": 0, + "elapsed_micros": 0, + "units_consumed": 0 + }, + "cancellation": null, + "effective_deadline": { + "expires_at": 1000 + }, + "ended_at": 20, + "started_at": 10, + "termination": "completed" + }, + "expected_state": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "idempotency_key": "settings-idempotency-fixture", + "payload": null, + "receipt": { + "actor": "actor.settings", + "catalog_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "committed_state": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "configuration_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "effect_class": "administrative", + "expected_state": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "external_proof": null, + "idempotency_key": "settings-idempotency-fixture", + "input_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "use-case.configuration.batch", + "outcome": "completed", + "policy_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "privacy_digest": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "request_id": "request.configuration.settings-fixture", + "scope": { + "project_id": "project.settings.scout", + "reference": null, + "repository_id": "repository.settings", + "scope_digest": "sha256:cb520e3731e8dbab143f4fc32b885fc977d467f4648a6429bf0ed9bf0294873e", + "worktree_id": "worktree.settings" + } + }, + "reconciliation": "reconciled" + } + }, + "current": { + "authorization": { + "outcome": "authorized" + }, + "coverage": { + "completeness": "complete", + "denominator": 1, + "eligible": 1, + "examined": 1, + "excluded": 0, + "matched": 1, + "omission_reasons": [], + "omitted": 0, + "unit": "configuration_control_plane", + "unknown": 0 + }, + "domain_state": "ready", + "freshness": { + "observed_at_micros": 100, + "state": "fresh", + "watermark": null + }, + "legal_actions": [ + { + "kind": "request_apply", + "operation": "configuration_batch" + }, + { + "kind": "refresh", + "operation": "configuration_list" + } + ], + "payload": { + "automation": { + "availability": { + "available": true + }, + "backend": "codex_app_server", + "config_endpoint": "/api/plugins/holographic/curation/config", + "configuration_revision_id": "rev-43", + "enabled": true, + "host_mode": "standalone" + }, + "environment": { + "global_accounting_enabled": false, + "global_accounting_mode": "disabled", + "pricing_offline": true, + "variables": [] + }, + "project": { + "config": { + "context_scout": false, + "exclude": [ + ".git/**", + ".tracedecay/**", + "**/*.min.*", + ".cache/**", + "**/.cache/**", + ".gradle/**", + "**/.gradle/**", + ".next/**", + "**/.next/**", + ".turbo/**", + "**/.turbo/**", + ".venv/**", + "**/.venv/**", + ".worktrees/**", + "**/.worktrees/**", + "__pycache__/**", + "**/__pycache__/**", + "build/**", + "**/build/**", + "coverage/**", + "**/coverage/**", + "dist/**", + "**/dist/**", + "node_modules/**", + "**/node_modules/**", + "out/**", + "**/out/**", + "target/**", + "**/target/**", + "vendor/**", + "**/vendor/**", + "venv/**", + "**/venv/**" + ], + "extract_docstrings": true, + "include": [], + "max_file_size": 2097152, + "sync": { + "auto_track_pr_branches": false, + "auto_track_pr_poll_secs": 300 + }, + "telemetry": { + "timings": true + }, + "track_call_sites": true + }, + "configuration_revision_id": "rev-43", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.fd56d9635ef3558e8743096f4bcc8dc5f6e38a457b3b225086af5bf4b01b75ab", + "pr_autotrack": { + "tracked": [] + } + }, + "resync_recommended": true, + "storage": { + "dashboard_root": "/project", + "graph_db": "/project/.tracedecay/graph.db", + "lcm_db": "/project/.tracedecay/lcm.db", + "lcm_scope": "project", + "memory_db": "/project/.tracedecay/memory.db", + "project_id": "project.settings.scout", + "project_root": "/project", + "savings_db": "/project/.tracedecay/savings.db", + "storage_mode": "project_local", + "store_root": "/project/.tracedecay" + }, + "user": { + "code_index_worker_configuration_revision_id": "workers-rev-3", + "code_index_worker_configuration_snapshot_id": "workers-snapshot-3", + "code_index_worker_status": null, + "code_index_workers": { + "mode": "automatic" + }, + "configuration_revision_id": "user-rev-7", + "configuration_snapshot_id": "user-snapshot-7", + "extraction_timeout_secs": 30, + "installed_agents": [], + "upload_enabled": false, + "watcher_debounce": "15s" + }, + "version": { + "cached_latest_version": null, + "channel": "stable", + "version": "0.1.0" + } + }, + "schema_revision": 1, + "scope": { + "project_id": "project.settings.scout", + "storage_mode": "project_local", + "store_root": "/project/.tracedecay" + }, + "source_watermark": null, + "time": { + "observation_time_micros": 100, + "valid_time_micros": null + }, + "version": { + "entity_version": null, + "graph_version": null + } + } + }, + "status": 200 + }, + "idempotency_conflict": { + "body": { + "contract": { + "schema_id": "schema.configuration.batch.result", + "schema_revision": 1 + }, + "problem": { + "cancellation_stage": null, + "code": "configuration.conflict", + "committed_receipt": null, + "coverage": null, + "detail": null, + "details": [], + "diagnostic": { + "code": "configuration.conflict", + "message": "The idempotency key was already used for a different settings change." + }, + "execution_failure_classification": null, + "kind": "conflict", + "legal_actions": [ + "refresh" + ], + "message": "The idempotency key was already used for a different settings change.", + "owning_layer": "application", + "request_id": "request.configuration.settings-fixture", + "retry": "after_revalidate", + "retry_after_millis": null, + "retry_scope": "fresh_request", + "retryable": true, + "revision": 1, + "terminality": "pre_admission", + "trace_id": "request.configuration.settings-fixture", + "unavailable_classification": null + }, + "request_id": "request.configuration.settings-fixture" + }, + "status": 409 + }, + "read": { + "body": { + "authorization": { + "outcome": "authorized" + }, + "coverage": { + "completeness": "complete", + "denominator": 1, + "eligible": 1, + "examined": 1, + "excluded": 0, + "matched": 1, + "omission_reasons": [], + "omitted": 0, + "unit": "configuration_control_plane", + "unknown": 0 + }, + "domain_state": "ready", + "freshness": { + "observed_at_micros": 100, + "state": "fresh", + "watermark": null + }, + "legal_actions": [ + { + "kind": "request_apply", + "operation": "configuration_batch" + }, + { + "kind": "refresh", + "operation": "configuration_list" + } + ], + "payload": { + "automation": { + "availability": { + "available": true + }, + "backend": "codex_app_server", + "config_endpoint": "/api/plugins/holographic/curation/config", + "configuration_revision_id": "rev-42", + "enabled": true, + "host_mode": "standalone" + }, + "environment": { + "global_accounting_enabled": false, + "global_accounting_mode": "disabled", + "pricing_offline": true, + "variables": [] + }, + "project": { + "config": { + "context_scout": false, + "exclude": [ + ".git/**", + ".tracedecay/**", + "**/*.min.*", + ".cache/**", + "**/.cache/**", + ".gradle/**", + "**/.gradle/**", + ".next/**", + "**/.next/**", + ".turbo/**", + "**/.turbo/**", + ".venv/**", + "**/.venv/**", + ".worktrees/**", + "**/.worktrees/**", + "__pycache__/**", + "**/__pycache__/**", + "build/**", + "**/build/**", + "coverage/**", + "**/coverage/**", + "dist/**", + "**/dist/**", + "node_modules/**", + "**/node_modules/**", + "out/**", + "**/out/**", + "target/**", + "**/target/**", + "vendor/**", + "**/vendor/**", + "venv/**", + "**/venv/**" + ], + "extract_docstrings": true, + "include": [], + "max_file_size": 1048576, + "sync": { + "auto_track_pr_branches": false, + "auto_track_pr_poll_secs": 300 + }, + "telemetry": { + "timings": true + }, + "track_call_sites": true + }, + "configuration_revision_id": "rev-42", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.fd56d9635ef3558e8743096f4bcc8dc5f6e38a457b3b225086af5bf4b01b75ab", + "pr_autotrack": { + "tracked": [] + } + }, + "resync_recommended": false, + "storage": { + "dashboard_root": "/project", + "graph_db": "/project/.tracedecay/graph.db", + "lcm_db": "/project/.tracedecay/lcm.db", + "lcm_scope": "project", + "memory_db": "/project/.tracedecay/memory.db", + "project_id": "project.settings.scout", + "project_root": "/project", + "savings_db": "/project/.tracedecay/savings.db", + "storage_mode": "project_local", + "store_root": "/project/.tracedecay" + }, + "user": { + "code_index_worker_configuration_revision_id": "workers-rev-3", + "code_index_worker_configuration_snapshot_id": "workers-snapshot-3", + "code_index_worker_status": null, + "code_index_workers": { + "mode": "automatic" + }, + "configuration_revision_id": "user-rev-7", + "configuration_snapshot_id": "user-snapshot-7", + "extraction_timeout_secs": 30, + "installed_agents": [], + "upload_enabled": false, + "watcher_debounce": "15s" + }, + "version": { + "cached_latest_version": null, + "channel": "stable", + "version": "0.1.0" + } + }, + "schema_revision": 1, + "scope": { + "project_id": "project.settings.scout", + "storage_mode": "project_local", + "store_root": "/project/.tracedecay" + }, + "source_watermark": null, + "time": { + "observation_time_micros": 100, + "valid_time_micros": null + }, + "version": { + "entity_version": null, + "graph_version": null + } + }, + "status": 200 + }, + "revision_conflict": { + "body": { + "actual_revision_id": "rev-43", + "code": "configuration_revision_conflict", + "detail": "settings changed after this edit began; refresh and retry", + "expected_revision_id": "rev-42" + }, + "status": 409 + }, + "unavailable": { + "body": { + "code": "configuration_authority_unavailable", + "detail": "configuration authority is unavailable" + }, + "status": 503 + }, + "unchanged": { + "body": { + "application_outcome": null, + "current": { + "authorization": { + "outcome": "authorized" + }, + "coverage": { + "completeness": "complete", + "denominator": 1, + "eligible": 1, + "examined": 1, + "excluded": 0, + "matched": 1, + "omission_reasons": [], + "omitted": 0, + "unit": "configuration_control_plane", + "unknown": 0 + }, + "domain_state": "ready", + "freshness": { + "observed_at_micros": 100, + "state": "fresh", + "watermark": null + }, + "legal_actions": [ + { + "kind": "request_apply", + "operation": "configuration_batch" + }, + { + "kind": "refresh", + "operation": "configuration_list" + } + ], + "payload": { + "automation": { + "availability": { + "available": true + }, + "backend": "codex_app_server", + "config_endpoint": "/api/plugins/holographic/curation/config", + "configuration_revision_id": "rev-42", + "enabled": true, + "host_mode": "standalone" + }, + "environment": { + "global_accounting_enabled": false, + "global_accounting_mode": "disabled", + "pricing_offline": true, + "variables": [] + }, + "project": { + "config": { + "context_scout": false, + "exclude": [ + ".git/**", + ".tracedecay/**", + "**/*.min.*", + ".cache/**", + "**/.cache/**", + ".gradle/**", + "**/.gradle/**", + ".next/**", + "**/.next/**", + ".turbo/**", + "**/.turbo/**", + ".venv/**", + "**/.venv/**", + ".worktrees/**", + "**/.worktrees/**", + "__pycache__/**", + "**/__pycache__/**", + "build/**", + "**/build/**", + "coverage/**", + "**/coverage/**", + "dist/**", + "**/dist/**", + "node_modules/**", + "**/node_modules/**", + "out/**", + "**/out/**", + "target/**", + "**/target/**", + "vendor/**", + "**/vendor/**", + "venv/**", + "**/venv/**" + ], + "extract_docstrings": true, + "include": [], + "max_file_size": 1048576, + "sync": { + "auto_track_pr_branches": false, + "auto_track_pr_poll_secs": 300 + }, + "telemetry": { + "timings": true + }, + "track_call_sites": true + }, + "configuration_revision_id": "rev-42", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.fd56d9635ef3558e8743096f4bcc8dc5f6e38a457b3b225086af5bf4b01b75ab", + "pr_autotrack": { + "tracked": [] + } + }, + "resync_recommended": false, + "storage": { + "dashboard_root": "/project", + "graph_db": "/project/.tracedecay/graph.db", + "lcm_db": "/project/.tracedecay/lcm.db", + "lcm_scope": "project", + "memory_db": "/project/.tracedecay/memory.db", + "project_id": "project.settings.scout", + "project_root": "/project", + "savings_db": "/project/.tracedecay/savings.db", + "storage_mode": "project_local", + "store_root": "/project/.tracedecay" + }, + "user": { + "code_index_worker_configuration_revision_id": "workers-rev-3", + "code_index_worker_configuration_snapshot_id": "workers-snapshot-3", + "code_index_worker_status": null, + "code_index_workers": { + "mode": "automatic" + }, + "configuration_revision_id": "user-rev-7", + "configuration_snapshot_id": "user-snapshot-7", + "extraction_timeout_secs": 30, + "installed_agents": [], + "upload_enabled": false, + "watcher_debounce": "15s" + }, + "version": { + "cached_latest_version": null, + "channel": "stable", + "version": "0.1.0" + } + }, + "schema_revision": 1, + "scope": { + "project_id": "project.settings.scout", + "storage_mode": "project_local", + "store_root": "/project/.tracedecay" + }, + "source_watermark": null, + "time": { + "observation_time_micros": 100, + "valid_time_micros": null + }, + "version": { + "entity_version": null, + "graph_version": null + } + } + }, + "status": 200 + }, + "validation": { + "body": { + "detail": "invalid project settings patch: unknown field `unknown_setting`, expected one of `expected_revision_id`, `idempotency_key`, `include`, `exclude`, `max_file_size`, `extract_docstrings`, `track_call_sites`, `telemetry`, `sync`, `context_scout`, `context_scout_intent`", + "validation_errors": [ + { + "field": "unknown_setting", + "message": "invalid project settings patch: unknown field `unknown_setting`, expected one of `expected_revision_id`, `idempotency_key`, `include`, `exclude`, `max_file_size`, `extract_docstrings`, `track_call_sites`, `telemetry`, `sync`, `context_scout`, `context_scout_intent`" + } + ] + }, + "status": 400 + } +} diff --git a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx index 370012bae5..27715acba8 100644 --- a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx +++ b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import settingsWire from '../../test/fixtures/settings-patch.json'; import { FIXTURES } from '../../../stories/fixtures/data.ts'; import { useScope, type ScopeWritability } from '../../data/scope/store.ts'; import { SettingsPage, findConfigSection } from './SettingsPage.tsx'; @@ -97,15 +98,7 @@ const FIXTURE_SETTINGS: ReadonlyArray = [ function projectPatchResponse(current: unknown) { return { - application_outcome: { - Effect: { - receipt: { - request_id: 'request.configuration.settings-fixture', - idempotency_key: 'settings-fixture', - outcome: 'completed', - }, - }, - }, + application_outcome: structuredClone(settingsWire.changed.body.application_outcome), current, }; } @@ -402,6 +395,35 @@ describe('SettingsPage effective configuration review', () => { ).toBeTruthy(); }); + it('requires a refresh after an uncertain effect without offering to repeat the write', async () => { + const response = structuredClone(settingsWire.changed.body); + response.application_outcome.value.reconciliation = 'pending'; + let dispatched = false; + const fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { + if (init?.method === 'PATCH') { + dispatched = true; + return jsonResponse(response); + } + return jsonResponse(dispatched ? updatedSettings('rev-43') : settings()); + }); + vi.stubGlobal('fetch', fetch); + const user = userEvent.setup(); + renderSettings(); + await openReview(user, MAX_FILE_SIZE); + await user.clear(proposal(MAX_FILE_SIZE)); + await user.type(proposal(MAX_FILE_SIZE), '2097152'); + await user.click(screen.getByRole('button', { name: 'Review project change' })); + await user.click(screen.getByRole('checkbox', { name: /I confirm this change against configuration revision rev-42/ })); + await user.click(screen.getByRole('button', { name: 'Apply project settings' })); + expect(await screen.findByText(/Refresh current values before retrying/)).toBeTruthy(); + expect(screen.queryByText('Project settings saved')).toBeNull(); + expect(screen.queryByRole('button', { name: 'Retry project settings' })).toBeNull(); + await user.click(screen.getByRole('button', { name: 'Load current values' })); + await waitFor(() => expect(within(row(MAX_FILE_SIZE)).getByText('2,097,152')).toBeTruthy()); + expect(fetch.mock.calls.filter(([, init]) => init?.method === 'PATCH')).toHaveLength(1); + expect(screen.queryByText('Project settings saved')).toBeNull(); + }); + it('blocks a stale project change before sending the patch', async () => { let getCount = 0; const methods: string[] = []; diff --git a/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx b/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx index 12a5b195c2..e2ae53be1f 100644 --- a/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx +++ b/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx @@ -397,7 +397,10 @@ function FrozenReview({ }) { const confirmed = settingsConfirmationHeld(state); const applying = state.status === 'submitting'; - const resolvable = state.status === 'conflicted' || state.status === 'review_superseded'; + const resolvable = state.status === 'conflicted' + || state.status === 'review_superseded' + || (state.status === 'submit_failed' + && (state.failure.kind === 'protocol_error' || state.failure.kind === 'unsettled')); const retryable = state.status === 'authority_withdrawn' || state.status === 'submit_failed'; return (
diff --git a/dashboard/src/workspaces/settings/settingsEditorMachine.ts b/dashboard/src/workspaces/settings/settingsEditorMachine.ts index c893afde88..fbccddee59 100644 --- a/dashboard/src/workspaces/settings/settingsEditorMachine.ts +++ b/dashboard/src/workspaces/settings/settingsEditorMachine.ts @@ -108,7 +108,7 @@ type SettingsResting = /** A write that did not reach a verdict. Distinct from a refusal, which did. */ type SettingsSubmitFailure = | { readonly kind: 'offline'; readonly detail: string } - | { readonly kind: 'error'; readonly detail: string } + | { readonly kind: 'error' | 'unsettled'; readonly detail: string } | { readonly kind: 'protocol_error'; readonly authority: string; readonly detail: string }; interface SettingsEditable { @@ -453,6 +453,8 @@ function settleSubmit( return failed(state, { kind: 'offline', detail: result.detail }); case 'error': return failed(state, { kind: 'error', detail: result.detail }); + case 'unsettled': + return failed(state, { kind: 'unsettled', detail: result.detail }); case 'protocol_error': return failed(state, { kind: 'protocol_error', diff --git a/dashboard/src/workspaces/settings/settingsMutation.test.ts b/dashboard/src/workspaces/settings/settingsMutation.test.ts new file mode 100644 index 0000000000..dae27d29a4 --- /dev/null +++ b/dashboard/src/workspaces/settings/settingsMutation.test.ts @@ -0,0 +1,89 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import wire from '../../test/fixtures/settings-patch.json'; +import { applySettingsMutation } from './settingsMutation.ts'; + +// settings_api/tests/wire.rs serializes these responses through Axum. Keep +// the complete receipts so decoding exercises the producer contract. +const request = { + writability: { state: 'writable', target: 'tracedecay' }, + scope: 'project', + expectedRevisionId: 'rev-42', + idempotencyKey: 'settings-idempotency-fixture', + readUrl: '/api/settings', + patchUrl: '/api/settings/project', + patch: { max_file_size: 2_097_152 }, +} as const; + +async function apply(response: { status: number; body: unknown }) { + const fetch = vi.fn() + .mockResolvedValueOnce(Response.json(wire.read.body, { status: wire.read.status })) + .mockResolvedValueOnce(Response.json(response.body, { status: response.status })); + vi.stubGlobal('fetch', fetch); + const result = await applySettingsMutation(request); + expect(fetch).toHaveBeenCalledTimes(2); + return result; +} + +afterEach(() => vi.unstubAllGlobals()); + +describe('settings producer/consumer settlement', () => { + it('accepts the committed effect and preserves its complete durable receipt', async () => { + expect(await apply(wire.changed)).toMatchObject({ + outcome: 'success', + revisionId: 'rev-43', + effectReceipt: wire.changed.body.application_outcome.value.receipt, + }); + }); + + it('accepts the explicit null outcome for an already-current patch', async () => { + expect(await apply(wire.unchanged)).toMatchObject({ outcome: 'success', revisionId: 'rev-42', effectReceipt: null }); + }); + + it('requires the explicit nullable settlement and the canonical effect shape', async () => { + for (const body of [ + { current: wire.changed.body.current }, + { ...wire.changed.body, application_outcome: { Effect: wire.changed.body.application_outcome.value } }, + ]) { + expect(await apply({ status: 200, body })).toMatchObject({ outcome: 'protocol_error' }); + } + }); + + it('does not present uncertain or unproven effects as applied', async () => { + for (const mutate of [ + (effect: typeof wire.changed.body.application_outcome.value) => { effect.execution.termination = 'effect_unknown'; }, + (effect: typeof wire.changed.body.application_outcome.value) => { effect.reconciliation = 'pending'; }, + (effect: typeof wire.changed.body.application_outcome.value) => { effect.receipt.outcome = 'partial'; }, + (effect: typeof wire.changed.body.application_outcome.value) => { Object.assign(effect.receipt, { committed_state: null }); }, + ]) { + const response = structuredClone(wire.changed); + mutate(response.body.application_outcome.value); + expect(await apply(response)).toMatchObject({ outcome: 'unsettled', detail: expect.stringContaining('Refresh current values before retrying') }); + } + }); + + it('retains validation, revision conflicts and unavailable authority semantics', async () => { + expect(await apply(wire.validation)).toMatchObject({ outcome: 'validation', errors: wire.validation.body.validation_errors }); + expect(await apply(wire.revision_conflict)).toEqual({ outcome: 'conflict', expectedRevisionId: 'rev-42', actualRevisionId: 'rev-43' }); + expect(await apply(wire.unavailable)).toMatchObject({ outcome: 'unavailable' }); + }); + + it('keeps idempotency conflict and cancellation messages distinct from revision conflicts', async () => { + for (const response of [wire.idempotency_conflict, wire.cancelled]) { + expect(await apply(response)).toEqual({ outcome: 'error', detail: response.body.problem.message }); + } + }); + + it('requires current values before retrying a problem carrying a committed effect', async () => { + const response = structuredClone(wire.idempotency_conflict); + Object.assign(response.body.problem, { + kind: 'partial_effect', + committed_receipt: { ...wire.changed.body.application_outcome.value.receipt, outcome: 'partial' }, + }); + expect(await apply(response)).toMatchObject({ outcome: 'unsettled', detail: expect.stringContaining('Refresh current values before retrying') }); + }); + + it('rejects problem/status disagreement and malformed refusals', async () => { + expect(await apply({ ...wire.idempotency_conflict, status: 503 })).toMatchObject({ outcome: 'protocol_error' }); + expect(await apply({ status: 409, body: {} })).toMatchObject({ outcome: 'protocol_error' }); + }); +}); diff --git a/dashboard/src/workspaces/settings/settingsMutation.ts b/dashboard/src/workspaces/settings/settingsMutation.ts index e6b90c6a38..7ad06da6dd 100644 --- a/dashboard/src/workspaces/settings/settingsMutation.ts +++ b/dashboard/src/workspaces/settings/settingsMutation.ts @@ -1,6 +1,11 @@ import { dashboardFetch } from '../../data/auth/credential.ts'; -import { z } from 'zod'; -import type { SettingsPayloadV1 } from '../../contracts/generated.ts'; +import { + DashboardConfigurationRefusalV1Schema, + ProjectSettingsPatchResponseV1Schema, + type EffectReceipt, + type SettingsPayloadV1, +} from '../../contracts/generated.ts'; +import { applicationProblemStatusMatches } from '../../contracts/application.ts'; import { readOnlyScopeRefusal, type ScopeWritability } from '../../data/scope/store.ts'; import { buildSettingsEditor, @@ -25,7 +30,7 @@ export type SettingsMutationResult = readonly resyncRecommended: boolean; readonly restartRecommended: boolean; /** Canonical durable application receipt for project writes. */ - readonly effectReceipt: Readonly> | null; + readonly effectReceipt: EffectReceipt | null; } | { readonly outcome: 'conflict'; @@ -41,6 +46,10 @@ export type SettingsMutationResult = readonly outcome: 'offline' | 'error'; readonly detail: string; } + | { + readonly outcome: 'unsettled'; + readonly detail: string; + } /** The authority that performs this write is not currently mounted. Kept * apart from `error`: nothing was attempted, and nothing changed. */ | { @@ -127,12 +136,6 @@ export async function applySettingsMutation( }), }); if (patched.outcome !== 'response') return patched; - if (patched.response.status === 409) { - return readConflict(patched.body, request.expectedRevisionId); - } - if (patched.response.status === 503) { - return { outcome: 'unavailable', detail: unavailableDetail(patched.body) }; - } // The gateway's read-only refusal, recognized by its own body rather than by // the status alone: a 405 this dashboard cannot account for stays a plain // error below rather than borrowing the scope explanation. @@ -144,13 +147,10 @@ export async function applySettingsMutation( detail: `Nothing was applied: ${scopeRefused.detail.replace(/\.$/, '')}.`, }; } + return { outcome: 'error', detail: 'Settings update failed (HTTP 405).' }; } if (!patched.response.ok) { - const validation = readValidation(patched.body); - return validation ?? { - outcome: 'error', - detail: `Settings update failed (HTTP ${patched.response.status}).`, - }; + return settingsRefusal(patched.body, patched.response.status, patchAuthority); } const projectPatch = request.scope === 'project' ? projectSettingsPatchResponse(patched.body, patchAuthority) @@ -186,23 +186,10 @@ function projectSettingsPatchResponse( | { readonly parsed: true; readonly current: unknown; - readonly effectReceipt: Readonly> | null; + readonly effectReceipt: EffectReceipt | null; } - | { readonly parsed: false; readonly violation: Extract } { - const response = z - .object({ - application_outcome: z.union([ - z.null(), - z.object({ - Effect: z.object({ - receipt: z.record(z.string(), z.unknown()), - }).passthrough(), - }).strict(), - ]), - current: z.unknown(), - }) - .strict() - .safeParse(body); + | { readonly parsed: false; readonly violation: Extract } { + const response = ProjectSettingsPatchResponseV1Schema.safeParse(body); if (!response.success) { return { parsed: false, @@ -212,10 +199,23 @@ function projectSettingsPatchResponse( ), }; } + const outcome = response.data.application_outcome; + if (outcome !== null && ( + outcome.outcome !== 'effect' + || outcome.value.execution.termination !== 'completed' + || outcome.value.reconciliation !== 'reconciled' + || outcome.value.receipt.outcome !== 'completed' + || outcome.value.receipt.committed_state === null + )) { + return { + parsed: false, + violation: { outcome: 'unsettled', detail: 'The settings effect is not confirmed. Refresh current values before retrying.' }, + }; + } return { parsed: true, current: response.data.current, - effectReceipt: response.data.application_outcome?.Effect.receipt ?? null, + effectReceipt: outcome?.outcome === 'effect' ? outcome.value.receipt : null, }; } @@ -282,42 +282,38 @@ function contractedPayload(body: unknown, authority: string): ContractedPayload } } -/** - * CONTRACT GAP, the settings refusal bodies are not generated. - * - * `/api/settings/{project,user}` build their 400/409/503 bodies with - * `serde_json::json!` in `crates/tracedecay-api/src/configuration.rs` - * (`settings_validation_error`, `configuration_revision_conflict_error`, - * `configuration_authority_unavailable_error`), so schemars never sees them - * and `contracts/generated.ts` carries no type for them. The success payload - * is contracted and is parsed as such above; only the refusals are read here. - * - * Every member below is therefore optional and every absence resolves to a - * stated unavailable value, a missing `actual_revision_id` reads as unknown, - * a missing `detail` says the authority is unavailable without naming which. - * None of it is inferred, and none of it turns a refusal into a success. - */ -const RefusalBodySchema = z - .object({ - detail: z.string().optional(), - expected_revision_id: z.string().optional(), - actual_revision_id: z.string().optional(), - validation_errors: z - .array(z.object({ field: z.string(), message: z.string() }).passthrough()) - .optional(), - }) - .passthrough(); - -function readRefusal(body: unknown): z.infer { - const parsed = RefusalBodySchema.safeParse(body); - return parsed.success ? parsed.data : {}; -} - -function unavailableDetail(body: unknown): string { - const detail = readRefusal(body).detail; - return detail != null && detail.length > 0 - ? `Nothing was applied: ${detail.replace(/\.$/, '')}.` - : 'Nothing was applied: the authority for this write is unavailable.'; +function settingsRefusal(body: unknown, status: number, authority: string): SettingsMutationResult { + const parsed = DashboardConfigurationRefusalV1Schema.safeParse(body); + if (!parsed.success) return protocolError(authority, 'expected a configuration refusal.'); + const refusal = parsed.data; + if ('problem' in refusal) { + // The existing settings adapter preserves HTTP 409 for cancellation. + const matches = refusal.problem.kind === 'cancelled' + ? status === 409 + : applicationProblemStatusMatches(status, refusal.problem.kind); + if (!matches) return protocolError(authority, 'the application problem disagrees with its HTTP status.'); + if (refusal.problem.committed_receipt !== null) { + return { outcome: 'unsettled', detail: `${refusal.problem.message} Refresh current values before retrying.` }; + } + return { outcome: 'error', detail: refusal.problem.message }; + } + if ('validation_errors' in refusal) { + return status === 400 + ? { outcome: 'validation', detail: refusal.detail, errors: refusal.validation_errors } + : protocolError(authority, 'the validation refusal disagrees with its HTTP status.'); + } + switch (refusal.code) { + case 'configuration_revision_conflict': + return status === 409 + ? { outcome: 'conflict', expectedRevisionId: refusal.expected_revision_id, actualRevisionId: refusal.actual_revision_id } + : protocolError(authority, 'the revision conflict disagrees with its HTTP status.'); + case 'configuration_authority_unavailable': + return status === 503 + ? { outcome: 'unavailable', detail: `Nothing was applied: ${refusal.detail.replace(/\.$/, '')}.` } + : protocolError(authority, 'the authority refusal disagrees with its HTTP status.'); + case 'application_contract_violation': + return protocolError(authority, refusal.detail); + } } type JsonFetchResult = @@ -357,31 +353,6 @@ function protocolError( }; } -function readValidation(body: unknown): SettingsMutationResult | null { - const refusal = readRefusal(body); - const errors: readonly SettingsValidationError[] = (refusal.validation_errors ?? []).map( - ({ field, message }) => ({ field, message }), - ); - if (errors.length === 0) return null; - return { - outcome: 'validation', - detail: refusal.detail ?? 'Settings validation failed.', - errors, - }; -} - -function readConflict( - body: unknown, - fallbackExpectedRevisionId: string, -): SettingsMutationResult { - const refusal = readRefusal(body); - return { - outcome: 'conflict', - expectedRevisionId: refusal.expected_revision_id ?? fallbackExpectedRevisionId, - actualRevisionId: refusal.actual_revision_id ?? null, - }; -} - const JSON_HEADERS = { accept: 'application/json', 'content-type': 'application/json', From 79a6779f8dbcfa1705f233926508e3d7be44e7f6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 18 Oct 2026 22:51:32 +0000 Subject: [PATCH 4/6] fix(settings): separate saved commits from reconciliation --- .../src/settings_api.rs | 2 - .../src/settings_api/tests/wire.rs | 272 ----------- .../runtime_surface_acceptance.rs | 117 ++++- .../src/test/fixtures/settings-patch.json | 450 ++++++++++-------- .../settings/SettingsPage.dom.test.tsx | 3 +- .../settings/SettingsReviewPanel.tsx | 2 + .../settings/settingsEditorMachine.test.ts | 3 + .../settings/settingsEditorMachine.ts | 3 + .../settings/settingsMutation.test.ts | 49 +- .../workspaces/settings/settingsMutation.ts | 12 +- 10 files changed, 415 insertions(+), 498 deletions(-) delete mode 100644 crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs diff --git a/crates/tracedecay-dashboard-api/src/settings_api.rs b/crates/tracedecay-dashboard-api/src/settings_api.rs index 09ee62a729..af0eed64e2 100644 --- a/crates/tracedecay-dashboard-api/src/settings_api.rs +++ b/crates/tracedecay-dashboard-api/src/settings_api.rs @@ -871,8 +871,6 @@ fn project_apply_error( #[cfg(test)] mod tests { - mod wire; - use super::*; use serde_json::json; use tracedecay_configuration::config::registry::ConfigurationRegistry; diff --git a/crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs b/crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs deleted file mode 100644 index 47682ebfde..0000000000 --- a/crates/tracedecay-dashboard-api/src/settings_api/tests/wire.rs +++ /dev/null @@ -1,272 +0,0 @@ -//! Serialized producer fixtures shared with the dashboard behavior tests. - -use axum::Json; -use axum::body::to_bytes; -use axum::response::IntoResponse; -use serde_json::{Value, json}; -use tracedecay_api::configuration::{ - configuration_application_problem_error, configuration_authority_unavailable_error, - configuration_revision_conflict_error, parse_project_settings_patch, -}; -use tracedecay_api::read_model::{ - DashboardCoverageV1, DashboardEnvelopeV1, DashboardLegalActionKindV1, - DashboardLegalActionRefV1, DashboardScopeV1, -}; -use tracedecay_contracts::{ - ApplicationOutcome, ApplicationProblem, ApplicationProblemEnvelope, AuthorityReceipt, - CapabilityGrantId, Deadline, DisclosureClass, EffectId, EffectReceipt, EffectResult, - EffectTermination, IdempotencyKey, OperationBudgetUsage, OperationReceipt, PolicyDecisionRef, - ReconciliationState, RequestId, ResolvedScope, ResultContractRef, -}; -use tracedecay_domain::configuration::{CodeIndexWorkerSelectionV1, ConfigurationRevisionId}; -use tracedecay_domain::{ - ActorId, ComponentVersion, ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, -}; -use tracedecay_tool_catalog::{EffectClass, SchemaId, UseCaseId}; - -use super::pinned_defaults; -use crate::settings_api::{ - EnvironmentSettingsPayloadV1, PrAutoTrackPayloadV1, ProjectSettingsPatchResponseV1, - ProjectSettingsPayloadV1, SettingsPayloadV1, StorageSettingsPayloadV1, UserSettingsPayloadV1, - VersionSettingsPayloadV1, automation_settings_payload, project_editable_settings, -}; - -fn settings(revision: &str, max_file_size: u64) -> DashboardEnvelopeV1 { - let pinned = pinned_defaults(&ConfigurationRevisionId::new(revision).unwrap()); - let mut config = project_editable_settings(&pinned); - config.max_file_size = max_file_size; - let payload = SettingsPayloadV1 { - project: ProjectSettingsPayloadV1 { - configuration_snapshot_id: pinned.snapshot().snapshot_id.as_str().to_owned(), - configuration_revision_id: revision.to_owned(), - config, - pr_autotrack: PrAutoTrackPayloadV1 { tracked: vec![] }, - }, - user: UserSettingsPayloadV1 { - configuration_snapshot_id: "user-snapshot-7".to_owned(), - configuration_revision_id: "user-rev-7".to_owned(), - code_index_worker_configuration_snapshot_id: "workers-snapshot-3".to_owned(), - code_index_worker_configuration_revision_id: "workers-rev-3".to_owned(), - upload_enabled: false, - code_index_workers: CodeIndexWorkerSelectionV1::Automatic {}, - code_index_worker_status: None, - watcher_debounce: "15s".to_owned(), - extraction_timeout_secs: 30, - installed_agents: vec![], - }, - automation: automation_settings_payload(&pinned), - environment: EnvironmentSettingsPayloadV1 { - global_accounting_mode: "disabled".to_owned(), - global_accounting_enabled: false, - pricing_offline: true, - variables: vec![], - }, - storage: StorageSettingsPayloadV1 { - project_id: Some("project.settings.scout".to_owned()), - project_root: "/project".to_owned(), - storage_mode: "project_local".to_owned(), - store_root: "/project/.tracedecay".to_owned(), - dashboard_root: "/project".to_owned(), - graph_db: "/project/.tracedecay/graph.db".to_owned(), - memory_db: "/project/.tracedecay/memory.db".to_owned(), - lcm_db: "/project/.tracedecay/lcm.db".to_owned(), - lcm_scope: "project".to_owned(), - savings_db: "/project/.tracedecay/savings.db".to_owned(), - }, - version: VersionSettingsPayloadV1 { - version: "0.1.0".to_owned(), - channel: "stable".to_owned(), - cached_latest_version: None, - }, - resync_recommended: Some(max_file_size != 1_048_576), - restart_recommended: None, - }; - let mut envelope = DashboardEnvelopeV1::ready( - DashboardScopeV1 { - project_id: Some("project.settings.scout".to_owned()), - storage_mode: "project_local".to_owned(), - store_root: "/project/.tracedecay".to_owned(), - }, - DashboardCoverageV1::complete(1, "configuration_control_plane"), - payload, - ) - .with_legal_actions(vec![ - DashboardLegalActionRefV1::new( - DashboardLegalActionKindV1::RequestApply, - "configuration_batch", - ), - DashboardLegalActionRefV1::new(DashboardLegalActionKindV1::Refresh, "configuration_list"), - ]); - envelope.time.observation_time_micros = 100; - envelope.freshness.observed_at_micros = Some(100); - envelope -} - -fn digest(character: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", character.to_string().repeat(64))).unwrap() -} - -fn effect() -> ApplicationOutcome { - let scope = ResolvedScope::new( - ProjectId::new("project.settings.scout").unwrap(), - RepositoryId::new("repository.settings").unwrap(), - WorktreeId::new("worktree.settings").unwrap(), - None, - ) - .unwrap(); - let idempotency_key = IdempotencyKey::new("settings-idempotency-fixture").unwrap(); - let authority = AuthorityReceipt { - grant_id: CapabilityGrantId::new("grant.settings").unwrap(), - grant_revision: 1, - grant_digest: digest('a'), - authorized_scope_digest: scope.scope_digest.clone(), - disclosure: DisclosureClass::Metadata, - policy: PolicyDecisionRef::new( - "policy.settings", - 1, - digest('b'), - ComponentVersion::new("policy.settings.v1").unwrap(), - ) - .unwrap(), - revalidated_at: UtcMicros(10), - }; - authority.validate_for(&scope).unwrap(); - ApplicationOutcome::Effect( - EffectResult::new( - EffectId::new("effect.configuration.settings").unwrap(), - EffectClass::Administrative, - idempotency_key.clone(), - authority, - digest('c'), - OperationReceipt::completed( - UtcMicros(10), - UtcMicros(20), - Deadline::new(UtcMicros(1_000)).unwrap(), - OperationBudgetUsage::default(), - ) - .unwrap(), - ReconciliationState::Reconciled, - EffectReceipt { - operation: UseCaseId::new("use-case.configuration.batch").unwrap(), - request_id: RequestId::new("request.configuration.settings-fixture").unwrap(), - actor: ActorId::new("actor.settings").unwrap(), - scope, - effect_class: EffectClass::Administrative, - idempotency_key, - input_digest: digest('a'), - expected_state: digest('c'), - policy_digest: digest('b'), - configuration_digest: digest('d'), - catalog_digest: digest('e'), - privacy_digest: digest('f'), - outcome: EffectTermination::Completed, - committed_state: Some(digest('1')), - external_proof: None, - }, - None, - ) - .unwrap(), - ) -} - -async fn http_fixture(response: impl IntoResponse) -> Value { - let response = response.into_response(); - let status = response.status().as_u16(); - let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap(); - let body: Value = serde_json::from_slice(&bytes).unwrap(); - json!({ "status": status, "body": body }) -} - -#[tokio::test] -async fn project_patch_serialization_preserves_settlement_and_refusals() { - let outcome = effect(); - let expected_receipt = match &outcome { - ApplicationOutcome::Effect(effect) => serde_json::to_value(&effect.receipt).unwrap(), - _ => unreachable!("fixture constructs an effect"), - }; - let changed = http_fixture(Json(ProjectSettingsPatchResponseV1 { - application_outcome: Some(outcome), - current: settings("rev-43", 2_097_152), - })) - .await; - assert_eq!(changed["status"], 200); - assert_eq!(changed["body"]["application_outcome"]["outcome"], "effect"); - assert_eq!( - changed["body"]["application_outcome"]["value"]["receipt"], - expected_receipt - ); - assert_eq!( - changed["body"]["current"]["payload"]["project"]["configuration_revision_id"], - "rev-43" - ); - let unchanged = http_fixture(Json(ProjectSettingsPatchResponseV1 { - application_outcome: None, - current: settings("rev-42", 1_048_576), - })) - .await; - assert!( - unchanged["body"] - .as_object() - .unwrap() - .contains_key("application_outcome") - ); - assert!(unchanged["body"]["application_outcome"].is_null()); - - let idempotency_conflict = ApplicationProblemEnvelope::new( - ResultContractRef::new( - SchemaId::new("schema.configuration.batch.result").unwrap(), - 1, - ) - .unwrap(), - RequestId::new("request.configuration.settings-fixture").unwrap(), - ApplicationProblem::conflict( - "configuration.conflict", - "The idempotency key was already used for a different settings change.", - ), - ) - .unwrap(); - let expected_problem = serde_json::to_value(&idempotency_conflict).unwrap(); - let idempotency = http_fixture(configuration_application_problem_error( - idempotency_conflict, - )) - .await; - assert_eq!(idempotency["status"], 409); - assert_eq!(idempotency["body"], expected_problem); - - let validation = parse_project_settings_patch(json!({ - "expected_revision_id": "rev-42", - "idempotency_key": "settings-idempotency-fixture", - "unknown_setting": true, - })) - .expect_err("unknown fields must be refused"); - let cancelled = ApplicationProblemEnvelope::new( - ResultContractRef::new( - SchemaId::new("schema.configuration.batch.result").unwrap(), - 1, - ) - .unwrap(), - RequestId::new("request.configuration.settings-fixture").unwrap(), - ApplicationProblem::cancelled_before_admission(), - ) - .unwrap(); - let fixtures = json!({ - "read": http_fixture(Json(settings("rev-42", 1_048_576))).await, - "changed": changed, - "unchanged": unchanged, - "revision_conflict": http_fixture(configuration_revision_conflict_error( - "settings changed after this edit began; refresh and retry", "rev-42", "rev-43" - )).await, - "validation": http_fixture(validation).await, - "unavailable": http_fixture(configuration_authority_unavailable_error()).await, - "idempotency_conflict": idempotency, - "cancelled": http_fixture(configuration_application_problem_error(cancelled)).await, - }); - // Bazel preserves this producer output for the frontend's shared fixture. - if let Some(output) = std::env::var_os("TEST_UNDECLARED_OUTPUTS_DIR") { - std::fs::write( - std::path::PathBuf::from(output).join("settings-patch.json"), - format!("{}\n", serde_json::to_string_pretty(&fixtures).unwrap()), - ) - .unwrap(); - } -} diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs index 1e4dbbfce7..b84be1c2fe 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs @@ -7,6 +7,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Output, Stdio}; +use axum::Json; use axum::body::{Body, to_bytes}; use axum::http::{Request, StatusCode}; use axum::response::IntoResponse; @@ -14,6 +15,10 @@ use serde_json::Value; use tempfile::TempDir; use tower::ServiceExt; use tracedecay::daemon::call_default_tool; +use tracedecay_api::configuration::{ + DashboardConfigurationRouteErrorV1, configuration_application_problem_error, + configuration_authority_unavailable_error, +}; use tracedecay_api::sse_response; use tracedecay_application::ProjectSourceAccessSnapshot; use tracedecay_application::feedback::concrete::open_feedback_runtime; @@ -33,10 +38,11 @@ use tracedecay_contracts::retrieval::PrimitiveRequest; #[cfg(all(unix, feature = "test-transport"))] use tracedecay_contracts::{ApplicationEnvelope, IdempotencyKey}; use tracedecay_contracts::{ - ApplicationOutcome, ApplicationProblemKind, CancellationContext, CancellationObservation, - CancellationSignal, CancellationStage, CapabilityGrantId, CapabilityGrantSnapshot, - CoverageCompleteness, Deadline, DisclosureClass, OperationBudgetUsage, OperationReceipt, - OperationTermination, PageRequest, RequestContext, RequestId, ResolvedScope, + ApplicationOutcome, ApplicationProblem, ApplicationProblemEnvelope, ApplicationProblemKind, + CancellationContext, CancellationObservation, CancellationSignal, CancellationStage, + CapabilityGrantId, CapabilityGrantSnapshot, CoverageCompleteness, Deadline, DisclosureClass, + OperationBudgetUsage, OperationReceipt, OperationTermination, PageRequest, RequestContext, + RequestId, ResolvedScope, ResultContractRef, }; use tracedecay_daemon_protocol::{ ApplicationSurfaceInvocationResult, ApplicationSurfaceRequest, FeedbackSurfaceRequest, @@ -73,7 +79,7 @@ use tracedecay_mcp::response_handles::{ResponseHandleLookup, retrieve_response_h use tracedecay_mcp::tools::dispatch::resolve_mcp_application_surface; use tracedecay_runtime_core::config::ProfileRoot; use tracedecay_tool_catalog::ApplicationSurfaceOperation; -use tracedecay_tool_catalog::{BindingSurface, CapabilityId, UseCaseId}; +use tracedecay_tool_catalog::{BindingSurface, CapabilityId, SchemaId, UseCaseId}; static DASHBOARD_CONFIGURATION_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); @@ -1012,6 +1018,7 @@ async fn dashboard_project_settings_commit_through_the_daemon_control_plane() { let (status, envelope) = get_dashboard_json(&agent, &settings_url); assert_eq!(status, 200, "GET settings failed: {envelope}"); + let read_fixture = serde_json::json!({ "status": status, "body": envelope }); let settings = &envelope["payload"]; let revision = settings["project"]["configuration_revision_id"] .as_str() @@ -1041,7 +1048,7 @@ async fn dashboard_project_settings_commit_through_the_daemon_control_plane() { "idempotency_key": "configuration.idempotency.dashboard-project-commit", "exclude": ["target/**", "dist/**"], "include": [".github/**"], - "max_file_size": 2048 + "max_file_size": 2_097_152 }), ); assert_eq!( @@ -1054,7 +1061,7 @@ async fn dashboard_project_settings_commit_through_the_daemon_control_plane() { let applied_payload = &applied["current"]["payload"]; assert_eq!(applied_payload["resync_recommended"], true); assert_eq!( - applied_payload["project"]["config"]["max_file_size"], 2048, + applied_payload["project"]["config"]["max_file_size"], 2_097_152, "the response must publish the daemon-returned snapshot: {applied_payload}" ); let applied_revision = applied_payload["project"]["configuration_revision_id"] @@ -1065,6 +1072,14 @@ async fn dashboard_project_settings_commit_through_the_daemon_control_plane() { applied_revision, revision, "a committed mutation must publish a new configuration revision" ); + let effect = &applied["application_outcome"]["value"]; + assert_eq!(applied["application_outcome"]["outcome"], "effect"); + assert_eq!(effect["execution"]["termination"], "completed"); + assert_eq!(effect["receipt"]["outcome"], "completed"); + assert!(effect["receipt"]["committed_state"].as_str().is_some()); + assert_eq!(effect["reconciliation"], "pending"); + assert_eq!(effect["payload"]["result_revision_id"], applied_revision); + let changed_fixture = serde_json::json!({ "status": status, "body": applied }); assert_ne!( applied_payload["project"]["config"]["max_file_size"], original_max_file_size, "the committed value must differ from the pre-change reading" @@ -1076,7 +1091,7 @@ async fn dashboard_project_settings_commit_through_the_daemon_control_plane() { assert_eq!(status, 200); assert_eq!( reloaded["payload"]["project"]["config"]["max_file_size"], - 2048 + 2_097_152 ); assert_eq!( reloaded["payload"]["project"]["config"]["include"][0], @@ -1087,6 +1102,60 @@ async fn dashboard_project_settings_commit_through_the_daemon_control_plane() { applied_revision.as_str() ); + let (unchanged_status, unchanged) = patch_dashboard_json( + &agent, + &project_url, + &serde_json::json!({ + "expected_revision_id": applied_revision, + "idempotency_key": "configuration.idempotency.dashboard-project-noop", + "max_file_size": 2_097_152 + }), + ); + assert_eq!( + unchanged_status, 200, + "unchanged project PATCH: {unchanged}" + ); + assert!(unchanged["application_outcome"].is_null()); + assert_eq!( + unchanged["current"]["payload"]["project"]["configuration_revision_id"], + applied_revision + ); + + let (idempotency_status, idempotency) = patch_dashboard_json( + &agent, + &project_url, + &serde_json::json!({ + "expected_revision_id": applied_revision, + "idempotency_key": "configuration.idempotency.dashboard-project-commit", + "max_file_size": 4096 + }), + ); + assert_eq!( + idempotency_status, 409, + "idempotency conflict: {idempotency}" + ); + assert_eq!(idempotency["problem"]["kind"], "conflict"); + + let (validation_status, validation) = patch_dashboard_json( + &agent, + &project_url, + &serde_json::json!({ + "expected_revision_id": applied_revision, + "idempotency_key": "configuration.idempotency.dashboard-project-invalid", + "unknown_setting": true + }), + ); + assert_eq!( + validation_status, 400, + "invalid project patch: {validation}" + ); + assert!( + !validation["validation_errors"] + .as_array() + .unwrap() + .is_empty() + ); + // CAS still holds against the real store: the superseded revision cannot // apply a second time. let (status, stale) = patch_dashboard_json( @@ -1102,6 +1171,38 @@ async fn dashboard_project_settings_commit_through_the_daemon_control_plane() { assert_eq!(stale["code"], "configuration_revision_conflict"); assert_eq!(stale["actual_revision_id"], applied_revision.as_str()); + // The changed response above comes from the mounted daemon, including its + // durable configuration effect and refreshed revision. The two admission + // refusals below use the same typed route factories as production. + let refusal = |(status, Json(body)): DashboardConfigurationRouteErrorV1| serde_json::json!({ "status": status.as_u16(), "body": body }); + let cancelled = ApplicationProblemEnvelope::new( + ResultContractRef::new( + SchemaId::new("schema.configuration.batch.result").unwrap(), + 1, + ) + .unwrap(), + RequestId::new("request.configuration.settings-cancelled").unwrap(), + ApplicationProblem::cancelled_before_admission(), + ) + .unwrap(); + let wire_fixture = serde_json::json!({ + "read": read_fixture, + "changed": changed_fixture, + "unchanged": { "status": unchanged_status, "body": unchanged }, + "revision_conflict": { "status": status, "body": stale }, + "validation": { "status": validation_status, "body": validation }, + "idempotency_conflict": { "status": idempotency_status, "body": idempotency }, + "unavailable": refusal(configuration_authority_unavailable_error()), + "cancelled": refusal(configuration_application_problem_error(cancelled)), + }); + if let Some(output) = std::env::var_os("TEST_UNDECLARED_OUTPUTS_DIR") { + std::fs::write( + PathBuf::from(output).join("settings-patch.json"), + format!("{}\n", serde_json::to_string_pretty(&wire_fixture).unwrap()), + ) + .unwrap(); + } + let _ = call_default_tool( fixture.profile(), &fixture.handshake, diff --git a/dashboard/src/test/fixtures/settings-patch.json b/dashboard/src/test/fixtures/settings-patch.json index 6c5d8473a9..ecfb089963 100644 --- a/dashboard/src/test/fixtures/settings-patch.json +++ b/dashboard/src/test/fixtures/settings-patch.json @@ -18,17 +18,17 @@ "legal_actions": [], "message": "The request was cancelled before admission", "owning_layer": "application", - "request_id": "request.configuration.settings-fixture", + "request_id": "request.configuration.settings-cancelled", "retry": "never", "retry_after_millis": null, "retry_scope": null, "retryable": false, "revision": 1, "terminality": "pre_admission", - "trace_id": "request.configuration.settings-fixture", + "trace_id": "request.configuration.settings-cancelled", "unavailable_classification": null }, - "request_id": "request.configuration.settings-fixture" + "request_id": "request.configuration.settings-cancelled" }, "status": 409 }, @@ -38,21 +38,21 @@ "outcome": "effect", "value": { "authority": { - "authorized_scope_digest": "sha256:cb520e3731e8dbab143f4fc32b885fc977d467f4648a6429bf0ed9bf0294873e", - "disclosure": "metadata", - "grant_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "grant_id": "grant.settings", + "authorized_scope_digest": "sha256:d6fb6d0938d4aef42e4eff3c0f113d607ce1baa34893752eae1361de955150ac", + "disclosure": "sensitive", + "grant_digest": "sha256:8c3dcc220cccaec81a373e117f12fd49517706ffc3a72c677b782ad0edcf423d", + "grant_id": "grant.daemon.configuration.ed4af84f307849aaff517d4b47c8e72002febe7cec0fcac38b788596cc343815", "grant_revision": 1, "policy": { - "decision_id": "policy.settings", - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "evaluator_revision": "policy.settings.v1", + "decision_id": "policy.daemon.configuration.v1", + "digest": "sha256:2dde51a3729d9b5fb9cb94b4eb70b6238971cf65bdf5a2ff5ba7d30cca42a36d", + "evaluator_revision": "tracedecay.daemon.configuration-policy.v1", "revision": 1 }, - "revalidated_at": 10 + "revalidated_at": 1792363717104188 }, - "effect_class": "administrative", - "effect_id": "effect.configuration.settings", + "effect_class": "configuration_write", + "effect_id": "effect.configuration.ed4af84f307849aaff517d4b47c8e72002febe7cec0fcac38b788596cc343815", "execution": { "budget": { "bytes_consumed": 0, @@ -61,39 +61,52 @@ }, "cancellation": null, "effective_deadline": { - "expires_at": 1000 + "expires_at": 1792363732101451 }, - "ended_at": 20, - "started_at": 10, + "ended_at": 1792363717104188, + "started_at": 1792363717104188, "termination": "completed" }, - "expected_state": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "idempotency_key": "settings-idempotency-fixture", - "payload": null, + "expected_state": "sha256:bae10ae7b2f476357fecbfc39d06eb7a43a89eb4b25d078422c77880d2199ef5", + "idempotency_key": "configuration.idempotency.dashboard-project-commit", + "payload": { + "base_revision_id": "configuration.initial.canonical.v1", + "created_at": 1792363717104188, + "effective_deadline_at": 1792363732101451, + "operation_digest": "sha256:95e5a654d365e7fafec21bd84bf68a159d13468d16e6cb38c26ac8493593727f", + "receipt_id": "configuration.receipt.v1.eaf7df3361e7130c1b2344d5537c8fdd09d4fb1288fdd4481bf6a0b0bfd9bbc8", + "result_revision_id": "configuration.revision.v1.f1dda57fc1603bacc602ad4cdbb08de9bfb8aa867eaabaef844d45f8c779d5b4", + "settlement_authority": { + "policy_digest": "sha256:2dde51a3729d9b5fb9cb94b4eb70b6238971cf65bdf5a2ff5ba7d30cca42a36d", + "policy_epoch": 1, + "revalidated_at": 1792363717104188 + }, + "snapshot_id": "tracedecay.configuration.snapshot.v1.061944e9d3ea29c90e551a2ce8af6d42a385839a986c5fd045ed0add7dee042f" + }, "receipt": { - "actor": "actor.settings", - "catalog_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "committed_state": "sha256:1111111111111111111111111111111111111111111111111111111111111111", - "configuration_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "effect_class": "administrative", - "expected_state": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "actor": "actor.tracedecay-daemon.project-open", + "catalog_digest": "sha256:57a70599bf2413cffb73f936d4b664b08f8b17533c2dcb0a2fd2bb078e528916", + "committed_state": "sha256:e3bd41f67533fc6a4941295fc75c48af569dfcf158b0d1cb209b9fe05fbac74e", + "configuration_digest": "sha256:e3bd41f67533fc6a4941295fc75c48af569dfcf158b0d1cb209b9fe05fbac74e", + "effect_class": "configuration_write", + "expected_state": "sha256:bae10ae7b2f476357fecbfc39d06eb7a43a89eb4b25d078422c77880d2199ef5", "external_proof": null, - "idempotency_key": "settings-idempotency-fixture", - "input_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "operation": "use-case.configuration.batch", + "idempotency_key": "configuration.idempotency.dashboard-project-commit", + "input_digest": "sha256:95e5a654d365e7fafec21bd84bf68a159d13468d16e6cb38c26ac8493593727f", + "operation": "use-case.application.configuration.batch", "outcome": "completed", - "policy_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "privacy_digest": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "request_id": "request.configuration.settings-fixture", + "policy_digest": "sha256:2dde51a3729d9b5fb9cb94b4eb70b6238971cf65bdf5a2ff5ba7d30cca42a36d", + "privacy_digest": "sha256:a7f16bc386eeafbe59c14451bd32da5458ced67a14d7ee7e2c641743da818eb6", + "request_id": "request.configuration.ed4af84f307849aaff517d4b47c8e72002febe7cec0fcac38b788596cc343815", "scope": { - "project_id": "project.settings.scout", - "reference": null, - "repository_id": "repository.settings", - "scope_digest": "sha256:cb520e3731e8dbab143f4fc32b885fc977d467f4648a6429bf0ed9bf0294873e", - "worktree_id": "worktree.settings" + "project_id": "proj_69173dcc8b282d79", + "reference": "refs/heads/master", + "repository_id": "repository.daemon.e684e4fa551644cdbc59164b7a49c80cee102ea4b2d784c374df6f1cc473e629", + "scope_digest": "sha256:d6fb6d0938d4aef42e4eff3c0f113d607ce1baa34893752eae1361de955150ac", + "worktree_id": "worktree.daemon.69173dcc8b282d79a6608867f4799d9d8a97885470a7019d268a97b38a01d0a3" } }, - "reconciliation": "reconciled" + "reconciliation": "pending" } }, "current": { @@ -114,7 +127,7 @@ }, "domain_state": "ready", "freshness": { - "observed_at_micros": 100, + "observed_at_micros": 1792363717173083, "state": "fresh", "watermark": null }, @@ -123,6 +136,10 @@ "kind": "request_apply", "operation": "configuration_batch" }, + { + "kind": "request_apply", + "operation": "profile_code_index_worker_selection" + }, { "kind": "refresh", "operation": "configuration_list" @@ -135,56 +152,46 @@ }, "backend": "codex_app_server", "config_endpoint": "/api/plugins/holographic/curation/config", - "configuration_revision_id": "rev-43", + "configuration_revision_id": "configuration.revision.v1.f1dda57fc1603bacc602ad4cdbb08de9bfb8aa867eaabaef844d45f8c779d5b4", "enabled": true, "host_mode": "standalone" }, "environment": { "global_accounting_enabled": false, - "global_accounting_mode": "disabled", + "global_accounting_mode": "disabled_by_env", "pricing_offline": true, - "variables": [] + "variables": [ + { + "active": true, + "description": "A truthy value disables global savings/accounting recording.", + "name": "TRACEDECAY_DISABLE_GLOBAL_DB", + "value": "1" + }, + { + "active": true, + "description": "Pins the global accounting database to an explicit path.", + "name": "TRACEDECAY_GLOBAL_DB", + "value": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/global.db" + }, + { + "active": true, + "description": "Pins the user-level TraceDecay data directory (default ~/.tracedecay).", + "name": "TRACEDECAY_DATA_DIR", + "value": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay" + } + ] }, "project": { "config": { "context_scout": false, "exclude": [ - ".git/**", - ".tracedecay/**", - "**/*.min.*", - ".cache/**", - "**/.cache/**", - ".gradle/**", - "**/.gradle/**", - ".next/**", - "**/.next/**", - ".turbo/**", - "**/.turbo/**", - ".venv/**", - "**/.venv/**", - ".worktrees/**", - "**/.worktrees/**", - "__pycache__/**", - "**/__pycache__/**", - "build/**", - "**/build/**", - "coverage/**", - "**/coverage/**", - "dist/**", - "**/dist/**", - "node_modules/**", - "**/node_modules/**", - "out/**", - "**/out/**", "target/**", - "**/target/**", - "vendor/**", - "**/vendor/**", - "venv/**", - "**/venv/**" + "dist/**" ], "extract_docstrings": true, - "include": [], + "include": [ + ".github/**" + ], "max_file_size": 2097152, "sync": { "auto_track_pr_branches": false, @@ -195,54 +202,63 @@ }, "track_call_sites": true }, - "configuration_revision_id": "rev-43", - "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.fd56d9635ef3558e8743096f4bcc8dc5f6e38a457b3b225086af5bf4b01b75ab", + "configuration_revision_id": "configuration.revision.v1.f1dda57fc1603bacc602ad4cdbb08de9bfb8aa867eaabaef844d45f8c779d5b4", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.061944e9d3ea29c90e551a2ce8af6d42a385839a986c5fd045ed0add7dee042f", "pr_autotrack": { "tracked": [] } }, "resync_recommended": true, "storage": { - "dashboard_root": "/project", - "graph_db": "/project/.tracedecay/graph.db", - "lcm_db": "/project/.tracedecay/lcm.db", - "lcm_scope": "project", - "memory_db": "/project/.tracedecay/memory.db", - "project_id": "project.settings.scout", - "project_root": "/project", - "savings_db": "/project/.tracedecay/savings.db", - "storage_mode": "project_local", - "store_root": "/project/.tracedecay" + "dashboard_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/dashboard", + "graph_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/tracedecay.db", + "lcm_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/sessions.db", + "lcm_scope": "profile_sharded", + "memory_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/tracedecay.db", + "project_id": "proj_69173dcc8b282d79", + "project_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/project", + "savings_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/global.db", + "storage_mode": "profile_sharded", + "store_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79" }, "user": { - "code_index_worker_configuration_revision_id": "workers-rev-3", - "code_index_worker_configuration_snapshot_id": "workers-snapshot-3", - "code_index_worker_status": null, + "code_index_worker_configuration_revision_id": "configuration.profile.initial.v1", + "code_index_worker_configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.3862ddb1c67746133efde2466f16da128b79ae5879a195e7075d1d0c9fe7525c", + "code_index_worker_status": { + "available_logical_cpus": 96, + "configured": { + "mode": "automatic" + }, + "effective_workers": 48, + "environment_override_workers": null, + "limiting_reason": "automatic_half_cores", + "memory_safe_workers": 384 + }, "code_index_workers": { "mode": "automatic" }, - "configuration_revision_id": "user-rev-7", - "configuration_snapshot_id": "user-snapshot-7", - "extraction_timeout_secs": 30, + "configuration_revision_id": "configuration.profile.initial.v1", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.3862ddb1c67746133efde2466f16da128b79ae5879a195e7075d1d0c9fe7525c", + "extraction_timeout_secs": 60, "installed_agents": [], "upload_enabled": false, - "watcher_debounce": "15s" + "watcher_debounce": "2s" }, "version": { "cached_latest_version": null, - "channel": "stable", - "version": "0.1.0" + "channel": "beta", + "version": "1.0.0-beta.88+451721797b8ae53bc539072fc19994df9cb66d34.dirty" } }, "schema_revision": 1, "scope": { - "project_id": "project.settings.scout", - "storage_mode": "project_local", - "store_root": "/project/.tracedecay" + "project_id": "proj_69173dcc8b282d79", + "storage_mode": "profile_sharded", + "store_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79" }, "source_watermark": null, "time": { - "observation_time_micros": 100, + "observation_time_micros": 1792363717173087, "valid_time_micros": null }, "version": { @@ -256,7 +272,7 @@ "idempotency_conflict": { "body": { "contract": { - "schema_id": "schema.configuration.batch.result", + "schema_id": "schema.application.configuration.configuration_batch.result", "schema_revision": 1 }, "problem": { @@ -268,26 +284,26 @@ "details": [], "diagnostic": { "code": "configuration.conflict", - "message": "The idempotency key was already used for a different settings change." + "message": "The configuration request conflicts with current state" }, "execution_failure_classification": null, "kind": "conflict", "legal_actions": [ "refresh" ], - "message": "The idempotency key was already used for a different settings change.", + "message": "The configuration request conflicts with current state", "owning_layer": "application", - "request_id": "request.configuration.settings-fixture", + "request_id": "request.dashboard.settings.7558e99727f379cfb4e9b326d15cd304.4", "retry": "after_revalidate", "retry_after_millis": null, "retry_scope": "fresh_request", "retryable": true, "revision": 1, "terminality": "pre_admission", - "trace_id": "request.configuration.settings-fixture", + "trace_id": "request.dashboard.settings.7558e99727f379cfb4e9b326d15cd304.4", "unavailable_classification": null }, - "request_id": "request.configuration.settings-fixture" + "request_id": "request.dashboard.settings.7558e99727f379cfb4e9b326d15cd304.4" }, "status": 409 }, @@ -310,7 +326,7 @@ }, "domain_state": "ready", "freshness": { - "observed_at_micros": 100, + "observed_at_micros": 1792363717093059, "state": "fresh", "watermark": null }, @@ -319,6 +335,10 @@ "kind": "request_apply", "operation": "configuration_batch" }, + { + "kind": "request_apply", + "operation": "profile_code_index_worker_selection" + }, { "kind": "refresh", "operation": "configuration_list" @@ -331,15 +351,34 @@ }, "backend": "codex_app_server", "config_endpoint": "/api/plugins/holographic/curation/config", - "configuration_revision_id": "rev-42", + "configuration_revision_id": "configuration.initial.canonical.v1", "enabled": true, "host_mode": "standalone" }, "environment": { "global_accounting_enabled": false, - "global_accounting_mode": "disabled", + "global_accounting_mode": "disabled_by_env", "pricing_offline": true, - "variables": [] + "variables": [ + { + "active": true, + "description": "A truthy value disables global savings/accounting recording.", + "name": "TRACEDECAY_DISABLE_GLOBAL_DB", + "value": "1" + }, + { + "active": true, + "description": "Pins the global accounting database to an explicit path.", + "name": "TRACEDECAY_GLOBAL_DB", + "value": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/global.db" + }, + { + "active": true, + "description": "Pins the user-level TraceDecay data directory (default ~/.tracedecay).", + "name": "TRACEDECAY_DATA_DIR", + "value": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay" + } + ] }, "project": { "config": { @@ -391,54 +430,62 @@ }, "track_call_sites": true }, - "configuration_revision_id": "rev-42", - "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.fd56d9635ef3558e8743096f4bcc8dc5f6e38a457b3b225086af5bf4b01b75ab", + "configuration_revision_id": "configuration.initial.canonical.v1", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.e027d2b7709ea9e063d5bbce4809ee1d4e72187298a22bd16df6720b2e329741", "pr_autotrack": { "tracked": [] } }, - "resync_recommended": false, "storage": { - "dashboard_root": "/project", - "graph_db": "/project/.tracedecay/graph.db", - "lcm_db": "/project/.tracedecay/lcm.db", - "lcm_scope": "project", - "memory_db": "/project/.tracedecay/memory.db", - "project_id": "project.settings.scout", - "project_root": "/project", - "savings_db": "/project/.tracedecay/savings.db", - "storage_mode": "project_local", - "store_root": "/project/.tracedecay" + "dashboard_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/dashboard", + "graph_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/tracedecay.db", + "lcm_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/sessions.db", + "lcm_scope": "profile_sharded", + "memory_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/tracedecay.db", + "project_id": "proj_69173dcc8b282d79", + "project_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/project", + "savings_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/global.db", + "storage_mode": "profile_sharded", + "store_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79" }, "user": { - "code_index_worker_configuration_revision_id": "workers-rev-3", - "code_index_worker_configuration_snapshot_id": "workers-snapshot-3", - "code_index_worker_status": null, + "code_index_worker_configuration_revision_id": "configuration.profile.initial.v1", + "code_index_worker_configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.3862ddb1c67746133efde2466f16da128b79ae5879a195e7075d1d0c9fe7525c", + "code_index_worker_status": { + "available_logical_cpus": 96, + "configured": { + "mode": "automatic" + }, + "effective_workers": 48, + "environment_override_workers": null, + "limiting_reason": "automatic_half_cores", + "memory_safe_workers": 384 + }, "code_index_workers": { "mode": "automatic" }, - "configuration_revision_id": "user-rev-7", - "configuration_snapshot_id": "user-snapshot-7", - "extraction_timeout_secs": 30, + "configuration_revision_id": "configuration.profile.initial.v1", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.3862ddb1c67746133efde2466f16da128b79ae5879a195e7075d1d0c9fe7525c", + "extraction_timeout_secs": 60, "installed_agents": [], "upload_enabled": false, - "watcher_debounce": "15s" + "watcher_debounce": "2s" }, "version": { "cached_latest_version": null, - "channel": "stable", - "version": "0.1.0" + "channel": "beta", + "version": "1.0.0-beta.88+451721797b8ae53bc539072fc19994df9cb66d34.dirty" } }, "schema_revision": 1, "scope": { - "project_id": "project.settings.scout", - "storage_mode": "project_local", - "store_root": "/project/.tracedecay" + "project_id": "proj_69173dcc8b282d79", + "storage_mode": "profile_sharded", + "store_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79" }, "source_watermark": null, "time": { - "observation_time_micros": 100, + "observation_time_micros": 1792363717093062, "valid_time_micros": null }, "version": { @@ -450,10 +497,10 @@ }, "revision_conflict": { "body": { - "actual_revision_id": "rev-43", + "actual_revision_id": "configuration.revision.v1.f1dda57fc1603bacc602ad4cdbb08de9bfb8aa867eaabaef844d45f8c779d5b4", "code": "configuration_revision_conflict", "detail": "settings changed after this edit began; refresh and retry", - "expected_revision_id": "rev-42" + "expected_revision_id": "configuration.initial.canonical.v1" }, "status": 409 }, @@ -485,7 +532,7 @@ }, "domain_state": "ready", "freshness": { - "observed_at_micros": 100, + "observed_at_micros": 1792363717205602, "state": "fresh", "watermark": null }, @@ -494,6 +541,10 @@ "kind": "request_apply", "operation": "configuration_batch" }, + { + "kind": "request_apply", + "operation": "profile_code_index_worker_selection" + }, { "kind": "refresh", "operation": "configuration_list" @@ -506,57 +557,47 @@ }, "backend": "codex_app_server", "config_endpoint": "/api/plugins/holographic/curation/config", - "configuration_revision_id": "rev-42", + "configuration_revision_id": "configuration.revision.v1.f1dda57fc1603bacc602ad4cdbb08de9bfb8aa867eaabaef844d45f8c779d5b4", "enabled": true, "host_mode": "standalone" }, "environment": { "global_accounting_enabled": false, - "global_accounting_mode": "disabled", + "global_accounting_mode": "disabled_by_env", "pricing_offline": true, - "variables": [] + "variables": [ + { + "active": true, + "description": "A truthy value disables global savings/accounting recording.", + "name": "TRACEDECAY_DISABLE_GLOBAL_DB", + "value": "1" + }, + { + "active": true, + "description": "Pins the global accounting database to an explicit path.", + "name": "TRACEDECAY_GLOBAL_DB", + "value": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/global.db" + }, + { + "active": true, + "description": "Pins the user-level TraceDecay data directory (default ~/.tracedecay).", + "name": "TRACEDECAY_DATA_DIR", + "value": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay" + } + ] }, "project": { "config": { "context_scout": false, "exclude": [ - ".git/**", - ".tracedecay/**", - "**/*.min.*", - ".cache/**", - "**/.cache/**", - ".gradle/**", - "**/.gradle/**", - ".next/**", - "**/.next/**", - ".turbo/**", - "**/.turbo/**", - ".venv/**", - "**/.venv/**", - ".worktrees/**", - "**/.worktrees/**", - "__pycache__/**", - "**/__pycache__/**", - "build/**", - "**/build/**", - "coverage/**", - "**/coverage/**", - "dist/**", - "**/dist/**", - "node_modules/**", - "**/node_modules/**", - "out/**", - "**/out/**", "target/**", - "**/target/**", - "vendor/**", - "**/vendor/**", - "venv/**", - "**/venv/**" + "dist/**" ], "extract_docstrings": true, - "include": [], - "max_file_size": 1048576, + "include": [ + ".github/**" + ], + "max_file_size": 2097152, "sync": { "auto_track_pr_branches": false, "auto_track_pr_poll_secs": 300 @@ -566,54 +607,63 @@ }, "track_call_sites": true }, - "configuration_revision_id": "rev-42", - "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.fd56d9635ef3558e8743096f4bcc8dc5f6e38a457b3b225086af5bf4b01b75ab", + "configuration_revision_id": "configuration.revision.v1.f1dda57fc1603bacc602ad4cdbb08de9bfb8aa867eaabaef844d45f8c779d5b4", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.061944e9d3ea29c90e551a2ce8af6d42a385839a986c5fd045ed0add7dee042f", "pr_autotrack": { "tracked": [] } }, "resync_recommended": false, "storage": { - "dashboard_root": "/project", - "graph_db": "/project/.tracedecay/graph.db", - "lcm_db": "/project/.tracedecay/lcm.db", - "lcm_scope": "project", - "memory_db": "/project/.tracedecay/memory.db", - "project_id": "project.settings.scout", - "project_root": "/project", - "savings_db": "/project/.tracedecay/savings.db", - "storage_mode": "project_local", - "store_root": "/project/.tracedecay" + "dashboard_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/dashboard", + "graph_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/tracedecay.db", + "lcm_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/sessions.db", + "lcm_scope": "profile_sharded", + "memory_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79/tracedecay.db", + "project_id": "proj_69173dcc8b282d79", + "project_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/project", + "savings_db": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/global.db", + "storage_mode": "profile_sharded", + "store_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79" }, "user": { - "code_index_worker_configuration_revision_id": "workers-rev-3", - "code_index_worker_configuration_snapshot_id": "workers-snapshot-3", - "code_index_worker_status": null, + "code_index_worker_configuration_revision_id": "configuration.profile.initial.v1", + "code_index_worker_configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.3862ddb1c67746133efde2466f16da128b79ae5879a195e7075d1d0c9fe7525c", + "code_index_worker_status": { + "available_logical_cpus": 96, + "configured": { + "mode": "automatic" + }, + "effective_workers": 48, + "environment_override_workers": null, + "limiting_reason": "automatic_half_cores", + "memory_safe_workers": 384 + }, "code_index_workers": { "mode": "automatic" }, - "configuration_revision_id": "user-rev-7", - "configuration_snapshot_id": "user-snapshot-7", - "extraction_timeout_secs": 30, + "configuration_revision_id": "configuration.profile.initial.v1", + "configuration_snapshot_id": "tracedecay.configuration.snapshot.v1.3862ddb1c67746133efde2466f16da128b79ae5879a195e7075d1d0c9fe7525c", + "extraction_timeout_secs": 60, "installed_agents": [], "upload_enabled": false, - "watcher_debounce": "15s" + "watcher_debounce": "2s" }, "version": { "cached_latest_version": null, - "channel": "stable", - "version": "0.1.0" + "channel": "beta", + "version": "1.0.0-beta.88+451721797b8ae53bc539072fc19994df9cb66d34.dirty" } }, "schema_revision": 1, "scope": { - "project_id": "project.settings.scout", - "storage_mode": "project_local", - "store_root": "/project/.tracedecay" + "project_id": "proj_69173dcc8b282d79", + "storage_mode": "profile_sharded", + "store_root": "/run/nl/w/4ba25a53-d0f6-4590-8e56-7dffb5d83e12/tmp/.tmpxOcJR3/home/.tracedecay/projects/proj_69173dcc8b282d79" }, "source_watermark": null, "time": { - "observation_time_micros": 100, + "observation_time_micros": 1792363717205605, "valid_time_micros": null }, "version": { diff --git a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx index 27715acba8..5859f7ac60 100644 --- a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx +++ b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx @@ -260,6 +260,7 @@ describe('SettingsPage effective configuration review', () => { await user.click(within(panel).getByRole('button', { name: 'Apply project settings' })); expect(await screen.findByText('Project settings saved')).toBeTruthy(); + expect(screen.getByText('Runtime reconciliation pending')).toBeTruthy(); expect(screen.getByText('Resync recommended')).toBeTruthy(); expect(reviewReadout()).toBe('applied'); // The effective value is the read-back, and the proposal is gone. @@ -397,7 +398,7 @@ describe('SettingsPage effective configuration review', () => { it('requires a refresh after an uncertain effect without offering to repeat the write', async () => { const response = structuredClone(settingsWire.changed.body); - response.application_outcome.value.reconciliation = 'pending'; + response.application_outcome.value.execution.termination = 'effect_unknown'; let dispatched = false; const fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { if (init?.method === 'PATCH') { diff --git a/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx b/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx index e2ae53be1f..c982d01c51 100644 --- a/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx +++ b/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx @@ -336,6 +336,8 @@ function WritableBody({ {applied.resyncRecommended ? Resync recommended : null} {applied.restartRecommended ? Restart recommended : null} + {applied.effectReconciliation === 'pending' ? Runtime reconciliation pending : null} + {applied.effectReconciliation === 'failed' ? Runtime reconciliation failed : null}

) : null} diff --git a/dashboard/src/workspaces/settings/settingsEditorMachine.test.ts b/dashboard/src/workspaces/settings/settingsEditorMachine.test.ts index 81a7d40796..351b3f0f5d 100644 --- a/dashboard/src/workspaces/settings/settingsEditorMachine.test.ts +++ b/dashboard/src/workspaces/settings/settingsEditorMachine.test.ts @@ -391,6 +391,7 @@ describe('settings editor: each verdict is its own state', () => { resyncRecommended: false, restartRecommended: false, effectReceipt: null, + effectReconciliation: null, }); expect(settled).toBe(editing); @@ -408,6 +409,7 @@ describe('settings editor: a save and a pending change cannot be shown at once', resyncRecommended: true, restartRecommended: false, effectReceipt: null, + effectReconciliation: null, }); it('records the save the authority reported, and nothing it did not', () => { @@ -420,6 +422,7 @@ describe('settings editor: a save and a pending change cannot be shown at once', revisionId: 'rev-43', resyncRecommended: true, restartRecommended: false, + effectReconciliation: null, }); expect(settingsRejection(state)).toBeNull(); expect(settingsReviewOf(state)).toBeNull(); diff --git a/dashboard/src/workspaces/settings/settingsEditorMachine.ts b/dashboard/src/workspaces/settings/settingsEditorMachine.ts index fbccddee59..270172e881 100644 --- a/dashboard/src/workspaces/settings/settingsEditorMachine.ts +++ b/dashboard/src/workspaces/settings/settingsEditorMachine.ts @@ -41,6 +41,7 @@ import { type UserSettingsChangeSet, type UserSettingsValues, } from './settingsModel.ts'; +import type { ReconciliationState } from '../../contracts/generated.ts'; import type { SettingsMutationRequest, SettingsMutationResult } from './settingsMutation.ts'; /** The two drafts the editor holds, always for the same authority snapshot. */ @@ -78,6 +79,7 @@ interface SettingsAppliedRecord { readonly revisionId: string; readonly resyncRecommended: boolean; readonly restartRecommended: boolean; + readonly effectReconciliation: ReconciliationState | null; } /** Field-level refusals, kept with the authority that issued them. */ @@ -418,6 +420,7 @@ function settleSubmit( revisionId: result.revisionId, resyncRecommended: result.resyncRecommended, restartRecommended: result.restartRecommended, + effectReconciliation: result.effectReconciliation, }, }); case 'validation': diff --git a/dashboard/src/workspaces/settings/settingsMutation.test.ts b/dashboard/src/workspaces/settings/settingsMutation.test.ts index dae27d29a4..fbae39842c 100644 --- a/dashboard/src/workspaces/settings/settingsMutation.test.ts +++ b/dashboard/src/workspaces/settings/settingsMutation.test.ts @@ -2,24 +2,31 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import wire from '../../test/fixtures/settings-patch.json'; import { applySettingsMutation } from './settingsMutation.ts'; -// settings_api/tests/wire.rs serializes these responses through Axum. Keep -// the complete receipts so decoding exercises the producer contract. +// runtime_surface_acceptance.rs captures the mounted daemon HTTP journey, +// including its durable commit, pending reconciliation, and refreshed revision. const request = { writability: { state: 'writable', target: 'tracedecay' }, scope: 'project', - expectedRevisionId: 'rev-42', - idempotencyKey: 'settings-idempotency-fixture', + expectedRevisionId: wire.read.body.payload.project.configuration_revision_id, + idempotencyKey: wire.changed.body.application_outcome.value.idempotency_key, readUrl: '/api/settings', patchUrl: '/api/settings/project', - patch: { max_file_size: 2_097_152 }, + patch: { + max_file_size: wire.changed.body.current.payload.project.config.max_file_size, + include: wire.changed.body.current.payload.project.config.include, + exclude: wire.changed.body.current.payload.project.config.exclude, + }, } as const; -async function apply(response: { status: number; body: unknown }) { +async function apply( + response: { status: number; body: unknown }, + current: typeof wire.read.body | typeof wire.changed.body.current = wire.read.body, +) { const fetch = vi.fn() - .mockResolvedValueOnce(Response.json(wire.read.body, { status: wire.read.status })) + .mockResolvedValueOnce(Response.json(current, { status: wire.read.status })) .mockResolvedValueOnce(Response.json(response.body, { status: response.status })); vi.stubGlobal('fetch', fetch); - const result = await applySettingsMutation(request); + const result = await applySettingsMutation({ ...request, expectedRevisionId: current.payload.project.configuration_revision_id }); expect(fetch).toHaveBeenCalledTimes(2); return result; } @@ -27,16 +34,23 @@ async function apply(response: { status: number; body: unknown }) { afterEach(() => vi.unstubAllGlobals()); describe('settings producer/consumer settlement', () => { - it('accepts the committed effect and preserves its complete durable receipt', async () => { + it('shows the real committed write as saved while reconciliation remains pending', async () => { + expect(wire.changed.body.application_outcome.value.reconciliation).toBe('pending'); expect(await apply(wire.changed)).toMatchObject({ outcome: 'success', - revisionId: 'rev-43', + revisionId: wire.changed.body.current.payload.project.configuration_revision_id, + effectReconciliation: 'pending', effectReceipt: wire.changed.body.application_outcome.value.receipt, }); }); it('accepts the explicit null outcome for an already-current patch', async () => { - expect(await apply(wire.unchanged)).toMatchObject({ outcome: 'success', revisionId: 'rev-42', effectReceipt: null }); + expect(await apply(wire.unchanged, wire.changed.body.current)).toMatchObject({ + outcome: 'success', + revisionId: wire.unchanged.body.current.payload.project.configuration_revision_id, + effectReceipt: null, + effectReconciliation: null, + }); }); it('requires the explicit nullable settlement and the canonical effect shape', async () => { @@ -51,7 +65,6 @@ describe('settings producer/consumer settlement', () => { it('does not present uncertain or unproven effects as applied', async () => { for (const mutate of [ (effect: typeof wire.changed.body.application_outcome.value) => { effect.execution.termination = 'effect_unknown'; }, - (effect: typeof wire.changed.body.application_outcome.value) => { effect.reconciliation = 'pending'; }, (effect: typeof wire.changed.body.application_outcome.value) => { effect.receipt.outcome = 'partial'; }, (effect: typeof wire.changed.body.application_outcome.value) => { Object.assign(effect.receipt, { committed_state: null }); }, ]) { @@ -61,9 +74,19 @@ describe('settings producer/consumer settlement', () => { } }); + it('requires the returned current revision to reflect the committed change', async () => { + const response = structuredClone(wire.changed); + response.body.current.payload.project.configuration_revision_id = request.expectedRevisionId; + expect(await apply(response)).toMatchObject({ outcome: 'unsettled' }); + }); + it('retains validation, revision conflicts and unavailable authority semantics', async () => { expect(await apply(wire.validation)).toMatchObject({ outcome: 'validation', errors: wire.validation.body.validation_errors }); - expect(await apply(wire.revision_conflict)).toEqual({ outcome: 'conflict', expectedRevisionId: 'rev-42', actualRevisionId: 'rev-43' }); + expect(await apply(wire.revision_conflict)).toEqual({ + outcome: 'conflict', + expectedRevisionId: wire.revision_conflict.body.expected_revision_id, + actualRevisionId: wire.revision_conflict.body.actual_revision_id, + }); expect(await apply(wire.unavailable)).toMatchObject({ outcome: 'unavailable' }); }); diff --git a/dashboard/src/workspaces/settings/settingsMutation.ts b/dashboard/src/workspaces/settings/settingsMutation.ts index 7ad06da6dd..5c1a384274 100644 --- a/dashboard/src/workspaces/settings/settingsMutation.ts +++ b/dashboard/src/workspaces/settings/settingsMutation.ts @@ -3,6 +3,7 @@ import { DashboardConfigurationRefusalV1Schema, ProjectSettingsPatchResponseV1Schema, type EffectReceipt, + type ReconciliationState, type SettingsPayloadV1, } from '../../contracts/generated.ts'; import { applicationProblemStatusMatches } from '../../contracts/application.ts'; @@ -31,6 +32,7 @@ export type SettingsMutationResult = readonly restartRecommended: boolean; /** Canonical durable application receipt for project writes. */ readonly effectReceipt: EffectReceipt | null; + readonly effectReconciliation: ReconciliationState | null; } | { readonly outcome: 'conflict'; @@ -153,7 +155,7 @@ export async function applySettingsMutation( return settingsRefusal(patched.body, patched.response.status, patchAuthority); } const projectPatch = request.scope === 'project' - ? projectSettingsPatchResponse(patched.body, patchAuthority) + ? projectSettingsPatchResponse(patched.body, patchAuthority, request.expectedRevisionId) : null; if (projectPatch && !projectPatch.parsed) return projectPatch.violation; const patchedPayload = contractedPayload( @@ -176,17 +178,20 @@ export async function applySettingsMutation( resyncRecommended: patchedPayload.payload.resync_recommended === true, restartRecommended: patchedPayload.payload.restart_recommended === true, effectReceipt: projectPatch?.parsed ? projectPatch.effectReceipt : null, + effectReconciliation: projectPatch?.parsed ? projectPatch.effectReconciliation : null, }; } function projectSettingsPatchResponse( body: unknown, authority: string, + expectedRevisionId: string, ): | { readonly parsed: true; readonly current: unknown; readonly effectReceipt: EffectReceipt | null; + readonly effectReconciliation: ReconciliationState | null; } | { readonly parsed: false; readonly violation: Extract } { const response = ProjectSettingsPatchResponseV1Schema.safeParse(body); @@ -200,12 +205,14 @@ function projectSettingsPatchResponse( }; } const outcome = response.data.application_outcome; + // Runtime reconciliation is separate from the durable configuration commit. + // A pending runtime update does not undo a saved, returned revision. if (outcome !== null && ( outcome.outcome !== 'effect' || outcome.value.execution.termination !== 'completed' - || outcome.value.reconciliation !== 'reconciled' || outcome.value.receipt.outcome !== 'completed' || outcome.value.receipt.committed_state === null + || response.data.current.payload.project.configuration_revision_id === expectedRevisionId )) { return { parsed: false, @@ -216,6 +223,7 @@ function projectSettingsPatchResponse( parsed: true, current: response.data.current, effectReceipt: outcome?.outcome === 'effect' ? outcome.value.receipt : null, + effectReconciliation: outcome?.outcome === 'effect' ? outcome.value.reconciliation : null, }; } From a14674cb0e111cf9c89a91f0f440a32693e6bb5e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 18 Oct 2026 22:55:51 +0000 Subject: [PATCH 5/6] fix(settings): label reconciliation as receipt state --- dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx | 2 +- dashboard/src/workspaces/settings/SettingsReviewPanel.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx index 5859f7ac60..14fd52604d 100644 --- a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx +++ b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx @@ -260,7 +260,7 @@ describe('SettingsPage effective configuration review', () => { await user.click(within(panel).getByRole('button', { name: 'Apply project settings' })); expect(await screen.findByText('Project settings saved')).toBeTruthy(); - expect(screen.getByText('Runtime reconciliation pending')).toBeTruthy(); + expect(screen.getByText('Receipt reconciliation pending')).toBeTruthy(); expect(screen.getByText('Resync recommended')).toBeTruthy(); expect(reviewReadout()).toBe('applied'); // The effective value is the read-back, and the proposal is gone. diff --git a/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx b/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx index c982d01c51..a96da1ec11 100644 --- a/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx +++ b/dashboard/src/workspaces/settings/SettingsReviewPanel.tsx @@ -336,8 +336,8 @@ function WritableBody({ {applied.resyncRecommended ? Resync recommended : null} {applied.restartRecommended ? Restart recommended : null} - {applied.effectReconciliation === 'pending' ? Runtime reconciliation pending : null} - {applied.effectReconciliation === 'failed' ? Runtime reconciliation failed : null} + {applied.effectReconciliation === 'pending' ? Receipt reconciliation pending : null} + {applied.effectReconciliation === 'failed' ? Receipt reconciliation failed : null}

) : null} From c4ffcea1b5bf75aca8e7e6103fe926f9d1fb7af3 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 18 Oct 2026 23:38:21 +0000 Subject: [PATCH 6/6] test(api): consume typed configuration refusals --- .../tests/dashboard_presentations.rs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/tracedecay-api/tests/dashboard_presentations.rs b/crates/tracedecay-api/tests/dashboard_presentations.rs index 962ab7d940..28d75a49af 100644 --- a/crates/tracedecay-api/tests/dashboard_presentations.rs +++ b/crates/tracedecay-api/tests/dashboard_presentations.rs @@ -1,7 +1,8 @@ use axum::http::StatusCode; use serde_json::json; use tracedecay_api::configuration::{ - configuration_revision_conflict_error, parse_project_settings_patch, parse_user_settings_patch, + ConfigurationRefusalV1, DashboardConfigurationRefusalV1, configuration_revision_conflict_error, + parse_project_settings_patch, parse_user_settings_patch, }; use tracedecay_api::feedback::{ FeedbackStatusCoverageV1, FeedbackStatusDenominatorsV1, FeedbackStatusPresentationV1, @@ -47,7 +48,12 @@ fn configuration_patch_and_cas_errors_are_api_owned() { ) .expect_err("unknown field must remain a typed bad request"); assert_eq!(shape_error.0, StatusCode::BAD_REQUEST); - assert_eq!(shape_error.1.0["validation_errors"][0]["field"], "unknown"); + assert!(matches!( + &shape_error.1.0, + DashboardConfigurationRefusalV1::Validation { .. } + )); + let shape_body = serde_json::to_value(shape_error.1.0).expect("serialize validation refusal"); + assert_eq!(shape_body["validation_errors"][0]["field"], "unknown"); let conflict = configuration_revision_conflict_error( "settings changed after this edit began; refresh and retry", @@ -55,9 +61,16 @@ fn configuration_patch_and_cas_errors_are_api_owned() { "revision.actual", ); assert_eq!(conflict.0, StatusCode::CONFLICT); - assert_eq!(conflict.1.0["code"], "configuration_revision_conflict"); - assert_eq!(conflict.1.0["expected_revision_id"], "revision.expected"); - assert_eq!(conflict.1.0["actual_revision_id"], "revision.actual"); + assert!(matches!( + &conflict.1.0, + DashboardConfigurationRefusalV1::Configuration( + ConfigurationRefusalV1::ConfigurationRevisionConflict { .. } + ) + )); + let conflict_body = serde_json::to_value(conflict.1.0).expect("serialize revision refusal"); + assert_eq!(conflict_body["code"], "configuration_revision_conflict"); + assert_eq!(conflict_body["expected_revision_id"], "revision.expected"); + assert_eq!(conflict_body["actual_revision_id"], "revision.actual"); } #[test]