@feltdb/core 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/agent-state.d.ts +489 -0
  2. package/dist/agent-state.d.ts.map +1 -0
  3. package/dist/agent-state.js +993 -0
  4. package/dist/build-identity.json +28 -0
  5. package/dist/cli/commands.js +88 -43
  6. package/dist/collection.d.ts +41 -4
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +116 -47
  9. package/dist/composition.d.ts.map +1 -1
  10. package/dist/composition.js +11 -1
  11. package/dist/create/build-identity.json +28 -0
  12. package/dist/create/cli.js +3 -3
  13. package/dist/create/create.js +17 -19
  14. package/dist/create/package-versions.js +1 -1
  15. package/dist/create/server-source/Cargo.lock +18 -3
  16. package/dist/create/server-source/Cargo.toml +4 -2
  17. package/dist/create/server-source/crates/feltdb/Cargo.toml +20 -0
  18. package/dist/create/server-source/crates/feltdb/LICENSE +21 -0
  19. package/dist/create/server-source/crates/feltdb/README.md +75 -0
  20. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +320 -0
  21. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +1545 -0
  22. package/dist/create/server-source/crates/feltdb/src/authority.rs +145 -15
  23. package/dist/create/server-source/crates/feltdb/src/lib.rs +238 -23
  24. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +4 -1
  25. package/dist/create/server-source/crates/feltdb/src/state_model.rs +850 -155
  26. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +1188 -0
  27. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +2884 -0
  28. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +534 -0
  29. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +23340 -0
  30. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +31 -13
  31. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +918 -0
  32. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +15 -2
  33. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +29 -0
  34. package/dist/create/server-source/crates/feltdb-client/LICENSE +21 -0
  35. package/dist/create/server-source/crates/feltdb-client/README.md +70 -0
  36. package/dist/create/server-source/crates/feltdb-client/src/application.rs +527 -0
  37. package/dist/create/server-source/crates/feltdb-client/src/authority.rs +679 -0
  38. package/dist/create/server-source/crates/feltdb-client/src/control.rs +292 -0
  39. package/dist/create/server-source/crates/feltdb-client/src/error.rs +227 -0
  40. package/dist/create/server-source/crates/feltdb-client/src/events.rs +146 -0
  41. package/dist/create/server-source/crates/feltdb-client/src/lib.rs +466 -0
  42. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/README.md +4 -0
  43. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/private-ca.pem +20 -0
  44. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server-key.pem +28 -0
  45. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server.pem +20 -0
  46. package/dist/create/server-source/crates/feltdb-client/tests/private_ca.rs +95 -0
  47. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +24 -1
  48. package/dist/create/server-source/crates/feltdb-server/LICENSE +21 -0
  49. package/dist/create/server-source/crates/feltdb-server/README.md +12 -1
  50. package/dist/create/server-source/crates/feltdb-server/benches/query_shape_observer.rs +136 -0
  51. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +3 -0
  52. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +6 -2
  53. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +21 -0
  54. package/dist/create/server-source/crates/feltdb-server/src/main.rs +75 -8
  55. package/dist/create/server-source/crates/feltdb-server/src/query_shape.rs +1137 -0
  56. package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.expected.json +300 -0
  57. package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.json +25 -0
  58. package/dist/create/server-source/crates/feltdb-server/tests/query_shape_trace.rs +128 -0
  59. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +683 -0
  60. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +3 -1
  61. package/dist/create/template/default-project/README.md +12 -7
  62. package/dist/create/template/default-project/src/pages/Dashboard.tsx +1 -1
  63. package/dist/create/template/dot-feltdb-README.md +3 -3
  64. package/dist/db.d.ts.map +1 -1
  65. package/dist/db.js +43 -17
  66. package/dist/development-runtime-bridge.d.ts.map +1 -1
  67. package/dist/development-runtime-bridge.js +22 -1
  68. package/dist/embedded-transaction.d.ts +75 -8
  69. package/dist/embedded-transaction.d.ts.map +1 -1
  70. package/dist/embedded-transaction.js +124 -18
  71. package/dist/error-codes.d.ts +2 -0
  72. package/dist/error-codes.d.ts.map +1 -1
  73. package/dist/error-codes.js +2 -0
  74. package/dist/feltdb.d.ts +1 -11
  75. package/dist/feltdb.d.ts.map +1 -1
  76. package/dist/file-db.d.ts +6 -0
  77. package/dist/file-db.d.ts.map +1 -1
  78. package/dist/file-db.js +43 -11
  79. package/dist/index-core.d.ts +1 -0
  80. package/dist/index-core.d.ts.map +1 -1
  81. package/dist/index-core.js +1 -0
  82. package/dist/indexeddb-db.d.ts.map +1 -1
  83. package/dist/indexeddb-db.js +65 -41
  84. package/dist/memory-db.d.ts.map +1 -1
  85. package/dist/memory-db.js +29 -8
  86. package/dist/studio/{KeyManagementPanel-DZuSeWBK.js → KeyManagementPanel-D5FMvvU9.js} +1 -0
  87. package/dist/studio/app.d.ts.map +1 -1
  88. package/dist/studio/components/GlobalSearch.d.ts.map +1 -1
  89. package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
  90. package/dist/studio/components/KeyManagementPanel.js +1 -1
  91. package/dist/studio/components/RouteErrorBoundary.d.ts +5 -0
  92. package/dist/studio/components/RouteErrorBoundary.d.ts.map +1 -0
  93. package/dist/studio/components/index.js +2 -2
  94. package/dist/studio/{components-BHTKlUgT.js → components-B4eKIUN5.js} +3 -2
  95. package/dist/studio/control-plane/Agents.d.ts.map +1 -1
  96. package/dist/studio/control-plane/Data.d.ts.map +1 -1
  97. package/dist/studio/control-plane/Instance.d.ts.map +1 -1
  98. package/dist/studio/control-plane/Operations.d.ts.map +1 -1
  99. package/dist/studio/control-plane/Overview.d.ts.map +1 -1
  100. package/dist/studio/control-plane/Schema.d.ts.map +1 -1
  101. package/dist/studio/control-plane/State.d.ts +11 -0
  102. package/dist/studio/control-plane/State.d.ts.map +1 -1
  103. package/dist/studio/control-plane/context.d.ts +4 -1
  104. package/dist/studio/control-plane/context.d.ts.map +1 -1
  105. package/dist/studio/control-plane/index.d.ts +1 -1
  106. package/dist/studio/control-plane/index.d.ts.map +1 -1
  107. package/dist/studio/control-plane/index.js +2 -2
  108. package/dist/studio/control-plane/primitives.d.ts +22 -2
  109. package/dist/studio/control-plane/primitives.d.ts.map +1 -1
  110. package/dist/studio/control-plane-fxMcJn74.js +4173 -0
  111. package/dist/studio/index.js +307 -235
  112. package/dist/studio/studio.css +1 -1
  113. package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
  114. package/dist/studio/utils/control-plane-api.js +4 -3
  115. package/dist/studio/utils/service-api.d.ts.map +1 -1
  116. package/dist/studio/utils/service-api.js +5 -4
  117. package/dist/studio-app/assets/{feltdb_wasm-DYfWAV0E.js → feltdb_wasm-BatwwogY.js} +1 -1
  118. package/dist/studio-app/assets/feltdb_wasm_bg-BX0newOr.wasm +0 -0
  119. package/dist/studio-app/assets/index-CiC8VY-a.css +1 -0
  120. package/dist/studio-app/assets/index-DoVvbIay.js +30 -0
  121. package/dist/studio-app/index.html +2 -2
  122. package/dist/telemetry.d.ts +1 -0
  123. package/dist/telemetry.d.ts.map +1 -1
  124. package/dist/telemetry.js +4 -3
  125. package/dist/transaction-identity.d.ts +62 -0
  126. package/dist/transaction-identity.d.ts.map +1 -0
  127. package/dist/transaction-identity.js +138 -0
  128. package/dist/transaction.d.ts +50 -5
  129. package/dist/transaction.d.ts.map +1 -1
  130. package/dist/transaction.js +28 -0
  131. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  132. package/dist/wasm/package.json +1 -1
  133. package/package.json +2 -2
  134. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  135. package/dist/studio/control-plane-DP8ljTDO.js +0 -3207
  136. package/dist/studio-app/assets/feltdb_wasm_bg-DHiOUhoQ.wasm +0 -0
  137. package/dist/studio-app/assets/index-6f6KVx5R.js +0 -30
  138. package/dist/studio-app/assets/index-BrsF-kWL.css +0 -1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FeltDB Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -5,7 +5,18 @@ second state store: every HTTP mutation calls `FeltDb::insert`, `FeltDb::update`
5
5
  `FeltDb::delete`, producing the same operations, vector-clock updates, durable log entries, and
6
6
  reactive events as an embedded mutation.
7
7
 
8
- Run it with:
8
+ Install it from crates.io (any Rust 1.96+ toolchain):
9
+
10
+ ```sh
11
+ cargo install feltdb-server --locked
12
+ feltdb-server --host 127.0.0.1 --port 7700 --namespace my-app --data ./data/feltdb.log
13
+ ```
14
+
15
+ The command line and the HTTP API are the supported contract. The library half of this crate exposes
16
+ the server's building blocks without a compatibility promise; for embedded state in your own process
17
+ depend on [`feltdb`](https://crates.io/crates/feltdb) instead.
18
+
19
+ From a checkout of the repository, run it with:
9
20
 
10
21
  ```sh
11
22
  cargo run -p feltdb-server -- \
@@ -0,0 +1,136 @@
1
+ //! Direct per-operation cost of the query-shape observer.
2
+ //!
3
+ //! This isolates what the observer adds to one `POST /query`: building the
4
+ //! structural input, normalizing, and recording one outcome. It is deliberately
5
+ //! not an end-to-end number — an HTTP round trip costs orders of magnitude more
6
+ //! and would hide it — and it says nothing about contention with the database's
7
+ //! state lock, which the observer never takes. The end-to-end comparison lives
8
+ //! in `tools/benchmarks/query-shape-observer.mjs`.
9
+ //!
10
+ //! cargo bench --locked -p feltdb-server --bench query_shape_observer
11
+ //!
12
+ //! Output is one JSON object. Each figure is the median of `ROUNDS` timed
13
+ //! rounds, in nanoseconds per operation, on this machine and build profile
14
+ //! only.
15
+
16
+ use feltdb_server::query_shape::{Access, ObserverConfig, QueryShapeObserver, ShapeInput};
17
+ use std::hint::black_box;
18
+ use std::sync::Arc;
19
+ use std::time::Instant;
20
+
21
+ const ROUNDS: usize = 7;
22
+ const OPERATIONS: usize = 200_000;
23
+
24
+ fn median(mut values: Vec<f64>) -> f64 {
25
+ values.sort_by(|a, b| a.partial_cmp(b).unwrap());
26
+ values[values.len() / 2]
27
+ }
28
+
29
+ /// One simulated query: the same `Vec` construction the handler performs.
30
+ fn observe(observer: &QueryShapeObserver, collection: &str, field: &str) {
31
+ let predicates = vec![(field, "eq"), ("kind", "eq")];
32
+ let order_by = vec![("n", "asc")];
33
+ let observation = observer.begin(&ShapeInput {
34
+ collection,
35
+ predicates: &predicates,
36
+ order_by: &order_by,
37
+ limit: 50,
38
+ has_semantic_clause: false,
39
+ });
40
+ observation.finish_executed(Access::Indexed, black_box(40), black_box(40));
41
+ }
42
+
43
+ fn per_op(mut run: impl FnMut(usize)) -> f64 {
44
+ median(
45
+ (0..ROUNDS)
46
+ .map(|_| {
47
+ let started = Instant::now();
48
+ for index in 0..OPERATIONS {
49
+ run(index);
50
+ }
51
+ started.elapsed().as_nanos() as f64 / OPERATIONS as f64
52
+ })
53
+ .collect(),
54
+ )
55
+ }
56
+
57
+ fn main() {
58
+ let observer = QueryShapeObserver::new(ObserverConfig::with_capacity(128));
59
+
60
+ // The cost of building the input with no observer at all: the floor.
61
+ let floor = per_op(|_| {
62
+ let predicates = vec![("tenant", "eq"), ("kind", "eq")];
63
+ let order_by = vec![("n", "asc")];
64
+ black_box((&predicates, &order_by));
65
+ });
66
+
67
+ let one_shape = per_op(|_| observe(&observer, "orders", "tenant"));
68
+
69
+ let fixed: Vec<String> = (0..64).map(|i| format!("c{i}")).collect();
70
+ let sixty_four_shapes = per_op(|index| observe(&observer, &fixed[index % 64], "tenant"));
71
+
72
+ // Every operation admits a new shape into a full table: the eviction scan.
73
+ let churn_names: Vec<String> = (0..4096).map(|i| format!("churn{i}")).collect();
74
+ let churn = QueryShapeObserver::new(ObserverConfig::with_capacity(128));
75
+ let eviction_churn = per_op(|index| observe(&churn, &churn_names[index % 4096], "tenant"));
76
+ let churn_max = QueryShapeObserver::new(ObserverConfig::with_capacity(1024));
77
+ let eviction_churn_max_capacity =
78
+ per_op(|index| observe(&churn_max, &churn_names[index % 4096], "tenant"));
79
+
80
+ let reserved = per_op(|_| {
81
+ let order_by = vec![("n", "asc")];
82
+ black_box(observer.begin(&ShapeInput {
83
+ collection: "_feltdb.Proposal",
84
+ predicates: &[],
85
+ order_by: &order_by,
86
+ limit: 50,
87
+ has_semantic_clause: false,
88
+ }));
89
+ });
90
+
91
+ // Contention: four threads recording into one observer.
92
+ let shared = Arc::new(QueryShapeObserver::new(ObserverConfig::with_capacity(128)));
93
+ let contended = median(
94
+ (0..ROUNDS)
95
+ .map(|_| {
96
+ let started = Instant::now();
97
+ let handles: Vec<_> = (0..4)
98
+ .map(|thread| {
99
+ let shared = shared.clone();
100
+ std::thread::spawn(move || {
101
+ let name = format!("t{thread}");
102
+ for _ in 0..OPERATIONS / 4 {
103
+ observe(&shared, &name, "tenant");
104
+ }
105
+ })
106
+ })
107
+ .collect();
108
+ for handle in handles {
109
+ handle.join().unwrap();
110
+ }
111
+ started.elapsed().as_nanos() as f64 / OPERATIONS as f64
112
+ })
113
+ .collect(),
114
+ );
115
+
116
+ let snapshot = churn_max.snapshot();
117
+ println!(
118
+ "{}",
119
+ serde_json::json!({
120
+ "benchmark": "query_shape_observer",
121
+ "profile": if cfg!(debug_assertions) { "debug" } else { "release" },
122
+ "operations_per_round": OPERATIONS,
123
+ "rounds": ROUNDS,
124
+ "unit": "ns/op (median of rounds)",
125
+ "input_construction_floor_no_observer": floor,
126
+ "begin_finish_one_shape": one_shape,
127
+ "begin_finish_64_shapes": sixty_four_shapes,
128
+ "begin_finish_every_op_evicts_capacity_128": eviction_churn,
129
+ "begin_finish_every_op_evicts_capacity_1024": eviction_churn_max_capacity,
130
+ "reserved_collection_skipped": reserved,
131
+ "four_threads_one_observer_wall_per_op": contended,
132
+ "capacity_1024_memory_bound_bytes": snapshot.memory_bound_bytes,
133
+ "capacity_1024_memory_estimate_bytes_at_full": snapshot.memory_estimate_bytes,
134
+ })
135
+ );
136
+ }
@@ -86,6 +86,9 @@ pub struct AppState {
86
86
  pub state_triggers: Arc<std::sync::Mutex<StateTriggerStore>>,
87
87
  pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
88
88
  pub readiness_probe: Arc<PathBuf>,
89
+ /// Opt-in, value-free query-shape observer for `POST /query`, or `None`
90
+ /// (the default). Diagnostic only: nothing reads it to answer a request.
91
+ pub query_shapes: Option<Arc<crate::query_shape::QueryShapeObserver>>,
89
92
  pub bounded_query_cursors: Arc<std::sync::Mutex<HashMap<String, BoundedQueryCursor>>>,
90
93
  /// The deployment's semantic runtime binding for bounded semantic query
91
94
  /// composition, or `None` when the deployment configures no runtime. A
@@ -411,11 +411,15 @@ fn copy_source(
411
411
  if let Some(parent) = destination.parent() {
412
412
  fs::create_dir_all(parent)?;
413
413
  }
414
- fs::copy(source, destination)?;
414
+ // Record what was copied, not what the source measured before the
415
+ // copy. An online backup copies files the server is still appending
416
+ // to (the audit log): a length taken from the source before copying
417
+ // would disagree with the copy's hash and fail verification.
418
+ let copied = fs::copy(source, destination)?;
415
419
  sync_file(destination)?;
416
420
  entries.push(BackupEntry {
417
421
  path: relative.into(),
418
- bytes: metadata.len(),
422
+ bytes: copied,
419
423
  sha256: hash_file(destination)?,
420
424
  });
421
425
  }
@@ -1,3 +1,23 @@
1
+ //! # feltdb-server
2
+ //!
3
+ //! The FeltDB server: the managed FeltDB authority over HTTP. Install and run it with
4
+ //!
5
+ //! ```sh
6
+ //! cargo install feltdb-server --locked
7
+ //! feltdb-server --host 127.0.0.1 --port 7700 --namespace my-app --data ./data/feltdb.log
8
+ //! ```
9
+ //!
10
+ //! Its supported contract is the binary's command line and its HTTP API (`feltdb.public.substrate@1.0`
11
+ //! and the `/v1` Service API, specified in the repository's
12
+ //! [`docs/reference/public-api.md`](https://github.com/rkendel1/flow_db/blob/main/docs/reference/public-api.md)).
13
+ //!
14
+ //! The modules below are the server's building blocks. They are public so the binary and the
15
+ //! repository's tests can use them, and **no compatibility promise covers them**: any release may
16
+ //! change them. For embedded state in your own process, depend on [`feltdb`] directly; it is
17
+ //! re-exported here at the exact version this server is built against.
18
+
19
+ pub use feltdb;
20
+
1
21
  pub mod app_state;
2
22
  pub mod application_contract;
3
23
  pub mod artifacts;
@@ -27,6 +47,7 @@ pub mod metrics;
27
47
  pub mod portable_bundle;
28
48
  pub mod principals;
29
49
  pub mod providers;
50
+ pub mod query_shape;
30
51
  pub mod releases;
31
52
  pub mod request_telemetry;
32
53
  pub mod sessions;
@@ -96,6 +96,7 @@ use feltdb_server::{
96
96
  portable_bundle::{BundleArtifact, BundleStore, ExportOptions, ImportMode, PortableBundle},
97
97
  principals::{PrincipalKind, PrincipalStatus},
98
98
  providers::{ProviderManifest, ProviderStatus, ProviderStore},
99
+ query_shape::{Access, ObserverConfig, QueryShapeObserver, ShapeInput},
99
100
  releases::{
100
101
  provider_for, AuthorizedDeploymentContext, DeploymentPlan, DeploymentProfile,
101
102
  DeploymentProvider, DeploymentProviderKind, DomainBinding, ReleaseManifest, ReleaseStore,
@@ -8766,6 +8767,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8766
8767
  // Constructed before the state so the same handle can be stopped after
8767
8768
  // serving: the pipeline outlives the router by exactly one shutdown.
8768
8769
  let audit_handle = AuditLog::new(config.audit.clone());
8770
+ // Opt-in and off by default. A malformed setting is a startup error rather
8771
+ // than a silent default, so a mistyped exclusion list cannot yield an
8772
+ // observer that records what the operator meant to exclude.
8773
+ let query_shapes = ObserverConfig::from_environment()?.map(|config| {
8774
+ println!(
8775
+ "feltdb: query-shape observer enabled (capacity {}, value-free, in-memory)",
8776
+ config.capacity
8777
+ );
8778
+ Arc::new(QueryShapeObserver::new(config))
8779
+ });
8769
8780
  let state = AppState {
8770
8781
  started_at: Instant::now(),
8771
8782
  admission: Arc::new(tokio::sync::Semaphore::new(config.max_in_flight)),
@@ -8829,6 +8840,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8829
8840
  state_triggers: Arc::new(std::sync::Mutex::new(state_trigger_store)),
8830
8841
  mesh: Arc::new(std::sync::Mutex::new(mesh_store)),
8831
8842
  readiness_probe: Arc::new(readiness_probe),
8843
+ query_shapes,
8832
8844
  bounded_query_cursors: Arc::new(std::sync::Mutex::new(HashMap::new())),
8833
8845
  semantic_runtime: configured_semantic_runtime()?,
8834
8846
  };
@@ -13520,7 +13532,7 @@ fn bounded_query_records(
13520
13532
  state: &AppState,
13521
13533
  request: &BoundedQueryRequest,
13522
13534
  execution: BoundedQueryExecution,
13523
- ) -> Result<Vec<feltdb::StoredRow>, ApiError> {
13535
+ ) -> Result<(Vec<feltdb::StoredRow>, Access), ApiError> {
13524
13536
  if execution != BoundedQueryExecution::Scan {
13525
13537
  let equalities = bounded_query_equalities(&request.conditions);
13526
13538
  let indexed =
@@ -13530,7 +13542,7 @@ fn bounded_query_records(
13530
13542
  bounded_query_matches(row, &request.conditions)
13531
13543
  })?;
13532
13544
  if let Some(records) = indexed {
13533
- return Ok(records);
13545
+ return Ok((records, Access::Indexed));
13534
13546
  }
13535
13547
  if execution == BoundedQueryExecution::Index {
13536
13548
  // Only reachable through the test-only header. A forced-index run
@@ -13541,11 +13553,12 @@ fn bounded_query_records(
13541
13553
  ));
13542
13554
  }
13543
13555
  }
13544
- Ok(state
13556
+ let scanned = state
13545
13557
  .db
13546
13558
  .query_collection(&request.collection, None, |row| {
13547
13559
  bounded_query_matches(row, &request.conditions)
13548
- })?)
13560
+ })?;
13561
+ Ok((scanned, Access::Scan))
13549
13562
  }
13550
13563
 
13551
13564
  fn issue_bounded_cursor(
@@ -13628,6 +13641,37 @@ async fn execute_bounded_query(
13628
13641
  feltdb::validate_semantic_clause(clause).map_err(semantic_query_error)?;
13629
13642
  }
13630
13643
  let hash = bounded_query_hash(&request)?;
13644
+ // Diagnostic only, and only when opted in. The observation sees structure
13645
+ // (names, operators, directions), never a value, and any failure inside it
13646
+ // is its own: nothing below reads it to decide a result. Dropped without
13647
+ // being finished — an early `?` return, a cancelled request — it records an
13648
+ // error outcome for the shape.
13649
+ let observation = state.query_shapes.as_deref().map(|observer| {
13650
+ let predicates: Vec<(&str, &str)> = request
13651
+ .conditions
13652
+ .iter()
13653
+ .filter_map(|condition| {
13654
+ condition
13655
+ .operators
13656
+ .keys()
13657
+ .next()
13658
+ .map(|operator| (condition.field.as_str(), operator.as_str()))
13659
+ })
13660
+ .collect();
13661
+ let order_by: Vec<(&str, &str)> = request
13662
+ .order_by
13663
+ .iter()
13664
+ .map(|order| (order.field.as_str(), order.direction.as_str()))
13665
+ .collect();
13666
+ observer.begin(&ShapeInput {
13667
+ collection: &request.collection,
13668
+ predicates: &predicates,
13669
+ order_by: &order_by,
13670
+ limit: request.limit,
13671
+ has_semantic_clause: request.semantic.is_some(),
13672
+ })
13673
+ });
13674
+ let mut executed: Option<(Access, usize)> = None;
13631
13675
  let (records, position) = if let Some(token) = &request.cursor {
13632
13676
  let cursor = state
13633
13677
  .bounded_query_cursors
@@ -13665,7 +13709,8 @@ async fn execute_bounded_query(
13665
13709
  // this surface's contract, so the matching set — not the collection, and
13666
13710
  // not an index bucket — is what gets sorted, and the limit is applied to
13667
13711
  // that ordered set. Nothing below this line knows which execution ran.
13668
- let matches = bounded_query_records(&state, &request, execution)?;
13712
+ let (matches, access) = bounded_query_records(&state, &request, execution)?;
13713
+ executed = Some((access, matches.len()));
13669
13714
  // Result materialization happens outside the state lock, so it is a cost
13670
13715
  // this request pays alone rather than one it imposes on every other
13671
13716
  // operation. PR36 measures it separately for exactly that reason.
@@ -13724,6 +13769,12 @@ async fn execute_bounded_query(
13724
13769
  } else {
13725
13770
  None
13726
13771
  };
13772
+ if let Some(observation) = observation {
13773
+ match executed {
13774
+ Some((access, matched)) => observation.finish_executed(access, matched, page.len()),
13775
+ None => observation.finish_continuation(page.len()),
13776
+ }
13777
+ }
13727
13778
  // The semantic stage transforms exactly this page. It runs after the
13728
13779
  // deterministic cursor is issued because continuation is by deterministic
13729
13780
  // position: the page may return fewer records than `limit` and still not
@@ -13814,6 +13865,15 @@ fn query_execution_diagnostics_router() -> Router<AppState> {
13814
13865
  get(query_execution_diagnostics),
13815
13866
  );
13816
13867
  }
13868
+ // Query-shape observations name collections and fields, so they are a
13869
+ // separate switch from the counters and exist only when the observer was
13870
+ // opted into. Counts and structure only; never a value.
13871
+ if ObserverConfig::from_environment().ok().flatten().is_some() {
13872
+ router = router.route(
13873
+ "/internal/query-shape-observations",
13874
+ get(query_shape_observations),
13875
+ );
13876
+ }
13817
13877
  // Phase attribution is a separate switch from the execution counters,
13818
13878
  // because it is a separate cost: the counters are a handful of atomic adds
13819
13879
  // per query, while attribution reads the clock at every instrumented
@@ -13833,6 +13893,15 @@ fn query_execution_diagnostics_router() -> Router<AppState> {
13833
13893
  router
13834
13894
  }
13835
13895
 
13896
+ /// The observer's retained shapes and counters. Diagnostic evidence about
13897
+ /// workload structure, not proof that any change would help.
13898
+ async fn query_shape_observations(State(state): State<AppState>) -> Json<Value> {
13899
+ match state.query_shapes.as_deref() {
13900
+ Some(observer) => Json(json!({ "enabled": true, "observations": observer.snapshot() })),
13901
+ None => Json(json!({ "enabled": false })),
13902
+ }
13903
+ }
13904
+
13836
13905
  /// Phase attribution: where elapsed time went, as counts and nanoseconds.
13837
13906
  ///
13838
13907
  /// Test instrumentation, mounted only under `FELTDB_WORKLOAD_DIAGNOSTICS=1`.
@@ -14224,9 +14293,7 @@ async fn commit_transaction(
14224
14293
  FlowError::CapabilityError(message) if message.starts_with("INVALID_READ_SET:") => {
14225
14294
  ApiError(StatusCode::BAD_REQUEST, message)
14226
14295
  }
14227
- FlowError::CapabilityError(message)
14228
- if message.starts_with("TRANSACTION_ID_PAYLOAD_MISMATCH:") =>
14229
- {
14296
+ FlowError::TransactionIdPayloadMismatch { .. } => {
14230
14297
  ApiError::structured(
14231
14298
  StatusCode::CONFLICT,
14232
14299
  json!({