Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
tweak_store.hpp
Go to the documentation of this file.
1// tweak_store.hpp — In-memory inspector tweak table and disk persistence.
2//
3// On-disk schema (mirrors @pulp/import-ir/src/tweaks.ts TweaksFile with
4// Pulp's bypass, lock, version, and source-tag extensions):
5//
6// {
7// "$schema": "pulp-tweaks://v1",
8// "version": 1,
9// "tweaks": Record<anchor, Record<dottedPath, value>>,
10// "bypassed": Record<anchor, true | string[]>,
11// "locked": string[] // optional
12// "sources": Record<anchor, Record<dottedPath, string>> // optional
13// }
14//
15// Bypass is a sibling overlay, not `applied:false` mixed into the
16// dotted-path tweak map. That preserves v1 backwards compatibility —
17// old files with only `tweaks` still load.
18//
19// `locked` is a third sibling overlay — a flat list of anchor ids the
20// user has marked as protected from bulk-clear / reimport. It mirrors
21// the bypass overlay shape (anchor-keyed, additive, omitted entirely
22// when empty) so old v1 files still load and a build that doesn't
23// understand `locked` simply ignores it.
24//
25// Atomic-write contract: save_to_disk() writes to `<path>.tmp` then
26// renames over the target so a crash mid-write never leaves a
27// half-flushed file at the canonical path.
28
29#pragma once
30
31#include <choc/containers/choc_Value.h>
32
33#include <cstddef>
34#include <cstdint>
35#include <mutex>
36#include <optional>
37#include <string>
38#include <string_view>
39#include <unordered_map>
40#include <unordered_set>
41#include <variant>
42#include <vector>
43
44namespace pulp::inspect {
45
54public:
56 struct Record {
57 std::string anchor_id;
58 std::string property_path; // dotted, e.g. "paint.backgroundColor"
59 choc::value::Value value;
60 std::string source; // optional adapter / origin tag (free-form)
61 };
62
66 using BypassValue = std::variant<bool, std::vector<std::string>>;
67
68 TweakStore() = default;
69
70 // ── Mutation ────────────────────────────────────────────────────
71
74 std::size_t apply_tweak(std::string_view anchor_id,
75 std::string_view property_path,
76 choc::value::Value value,
77 std::string_view source = {});
78
81 struct BatchEntry {
82 std::string property_path;
83 choc::value::Value value;
84 };
85
102 std::size_t apply_tweaks_batch(std::string_view anchor_id,
103 std::vector<BatchEntry> entries,
104 std::string_view source = {});
105
108 bool remove_tweak(std::string_view anchor_id,
109 std::string_view property_path);
110
113 std::size_t remove_anchor(std::string_view anchor_id);
114
116 void clear();
117
122 void set_bypass(std::string_view anchor_id, BypassValue value);
123
126 void clear_bypass(std::string_view anchor_id);
127
134 void set_locked(std::string_view anchor_id, bool locked);
135
138 void clear_lock(std::string_view anchor_id);
139
140 // ── Inspection ──────────────────────────────────────────────────
141
143 std::size_t count() const;
144
149 std::vector<Record> list_tweaks() const;
150
153 std::optional<choc::value::Value>
154 lookup(std::string_view anchor_id, std::string_view property_path) const;
155
158 bool is_bypassed(std::string_view anchor_id,
159 std::string_view property_path) const;
160
163 std::optional<BypassValue>
164 bypass_for(std::string_view anchor_id) const;
165
173 std::vector<std::string> bypassed_anchors() const;
174
176 bool is_locked(std::string_view anchor_id) const;
177
182 std::vector<std::string> locked_anchors() const;
183
184 // ── Disk persistence ────────────────────────────────────────────
185
187 static constexpr int kSchemaVersion = 1;
188
191 struct DiskResult {
192 bool ok = false;
193 std::string error;
194 std::size_t tweak_count = 0;
195 std::size_t bypass_count = 0;
196 std::string path; // resolved path actually used
197 };
198
204 DiskResult load_from_disk(std::string_view path = {});
205
211 DiskResult save_to_disk(std::string_view path = {}) const;
212
221 void set_auto_save(bool enabled, std::string_view path = {});
222
224 bool auto_save_enabled() const;
225
228 std::string auto_save_path() const;
229
236 static std::string default_tweaks_path();
237
241 std::string to_json() const;
242
246 DiskResult from_json(std::string_view json);
247
248 // ── Drift detection ─────────────────────────────────────────────
249 //
250 // A tweak is keyed by (anchor_id, property_path). After a design
251 // re-import the live view tree may no longer carry an anchor a
252 // stored tweak references — that tweak is "orphaned" and silently
253 // does nothing. The drift API surfaces these so the inspector
254 // drawer + `pulp tweaks diff` CLI can warn the user.
255
262
264 static const char* drift_reason_str(DriftReason reason);
265
269 std::string anchor_id;
270 std::string property_path;
271 choc::value::Value value;
272 std::string source;
274 };
275
277 struct DriftReport {
278 std::vector<Record> clean;
279 std::vector<DriftedTweak> drifted;
280 std::vector<DriftedTweak> orphaned;
281
282 std::size_t total() const {
283 return clean.size() + drifted.size() + orphaned.size();
284 }
285 bool has_drift() const {
286 return !drifted.empty() || !orphaned.empty();
287 }
288 };
289
301 std::unordered_set<std::string> anchors;
302 std::unordered_map<std::string, std::unordered_set<std::string>>
304 };
305
310
314 DriftReport diff(const std::vector<std::string>& live_anchors) const;
315
318 std::vector<DriftedTweak> find_drifted(const DesignSnapshot& design) const;
319
321 std::vector<DriftedTweak>
322 find_drifted(const std::vector<std::string>& live_anchors) const;
323
328 static std::string drift_report_to_json(const DriftReport& report);
329
330private:
331 mutable std::mutex mtx_;
332 // Outer key: anchor_id. Inner key: dotted property path.
333 // Inner value: the most-recent assigned value + source.
334 struct Entry {
335 choc::value::Value value;
336 std::string source;
337 std::uint64_t sequence = 0;
338 };
339 std::unordered_map<std::string,
340 std::unordered_map<std::string, Entry>> tweaks_;
341 std::unordered_map<std::string, BypassValue> bypassed_;
342 // Anchor ids the user has marked as locked. A flat set means lock
343 // has no per-path granularity — it protects a whole anchor.
344 std::unordered_set<std::string> locked_;
345 std::uint64_t next_sequence_ = 0;
346
347 // Auto-save state. When `auto_save_` is true, every successful
348 // mutation re-flushes the table to `auto_save_path_` after the
349 // mutation completes. Both fields are guarded by `mtx_`.
350 bool auto_save_ = false;
351 std::string auto_save_path_;
352
353 // Batch suspension depth. While > 0, maybe_auto_save_unlocked() is
354 // a no-op so a multi-key batch flushes disk exactly once, after the
355 // last key is written. Guarded by mtx_. A counter (not a bool)
356 // keeps nested batches correct.
357 int auto_save_suspend_depth_ = 0;
358
359 // Helpers (assume mtx_ held by caller unless noted).
360 std::string to_json_locked() const;
361 DiskResult from_json_locked(std::string_view json);
362 DiskResult save_locked(std::string_view path) const;
363 // Auto-save trigger. Called by every mutating public method AFTER
364 // it releases its lock; takes its own lock briefly to read auto-
365 // save state. Returns silently if auto-save is off.
366 void maybe_auto_save_unlocked() const;
367};
368
369} // namespace pulp::inspect
std::string auto_save_path() const
The path auto-save flushes to (empty if auto-save is off and no explicit path has ever been set).
void set_locked(std::string_view anchor_id, bool locked)
Set the lock state for an anchor.
std::vector< std::string > bypassed_anchors() const
Return every anchor id that currently has a bypass entry, regardless of whether it also has tweak rec...
DiskResult load_from_disk(std::string_view path={})
Replace the in-memory state with the contents of path.
void set_auto_save(bool enabled, std::string_view path={})
Enable / disable post-mutation auto-save.
bool auto_save_enabled() const
Whether auto-save is currently armed.
std::size_t remove_anchor(std::string_view anchor_id)
Remove all entries for an anchor.
std::vector< Record > list_tweaks() const
Return every record in the store's stable order.
std::vector< DriftedTweak > find_drifted(const std::vector< std::string > &live_anchors) const
Convenience overload taking a flat anchor list.
static const char * drift_reason_str(DriftReason reason)
Stringify a DriftReason for JSON / human output.
std::size_t apply_tweaks_batch(std::string_view anchor_id, std::vector< BatchEntry > entries, std::string_view source={})
Apply several tweaks under one anchor as a SINGLE atomic unit.
DriftReport diff(const DesignSnapshot &design) const
Classify every stored tweak against design.
std::size_t apply_tweak(std::string_view anchor_id, std::string_view property_path, choc::value::Value value, std::string_view source={})
Record an edit.
std::size_t count() const
Total number of (anchor, property) entries.
bool remove_tweak(std::string_view anchor_id, std::string_view property_path)
Remove a single (anchor_id, property_path) entry.
std::variant< bool, std::vector< std::string > > BypassValue
Bypass overlay value — matches the TS true | string[] shape.
Definition tweak_store.hpp:66
std::string to_json() const
Serialize the current state to a JSON string in the on-disk schema.
static std::string drift_report_to_json(const DriftReport &report)
Render a DriftReport as a JSON string (used by pulp tweaks diff --json and the protocol layer).
bool is_bypassed(std::string_view anchor_id, std::string_view property_path) const
Whether a specific (anchor_id, property_path) is currently bypassed (either whole-anchor or path-spec...
void clear_bypass(std::string_view anchor_id)
Convenience: clear the bypass for an anchor (equivalent to set_bypass(id, false)).
static std::string default_tweaks_path()
Resolve the default pulp-tweaks.json location:
void clear_lock(std::string_view anchor_id)
Convenience: clear the lock for an anchor (equivalent to set_locked(id, false)).
void clear()
Clear the entire table (tweaks + bypass overlay + lock set).
bool is_locked(std::string_view anchor_id) const
Whether anchor_id is currently locked.
std::optional< choc::value::Value > lookup(std::string_view anchor_id, std::string_view property_path) const
Look up a specific (anchor_id, property_path).
std::vector< DriftedTweak > find_drifted(const DesignSnapshot &design) const
Return only the orphaned + drifted tweaks for design — the subset the inspector drift drawer renders.
DriftReason
Why a tweak failed to apply cleanly.
Definition tweak_store.hpp:257
@ anchor_not_found
No live view carries this anchor_id.
Definition tweak_store.hpp:258
@ property_not_found
Anchor resolves but the design no longer exposes this property path.
Definition tweak_store.hpp:259
std::vector< std::string > locked_anchors() const
Return every anchor id that currently carries a lock, in unspecified order.
void set_bypass(std::string_view anchor_id, BypassValue value)
Set the bypass state for an anchor.
static constexpr int kSchemaVersion
On-disk schema version. Bumped if/when we ever break the format.
Definition tweak_store.hpp:187
DiskResult from_json(std::string_view json)
Replace state from an in-memory JSON string.
std::optional< BypassValue > bypass_for(std::string_view anchor_id) const
Current bypass state for an anchor.
DriftReport diff(const std::vector< std::string > &live_anchors) const
Convenience: classify against a flat anchor list (anchor-only matching — no property-level drift).
DiskResult save_to_disk(std::string_view path={}) const
Atomically write the current in-memory state to path.
@ path
Definition scene_node.hpp:93
Definition design_adherence.hpp:32
Definition agent_context.hpp:7
Definition host_transport_projector.hpp:8
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
One (property_path, value) pair within a batched apply.
Definition tweak_store.hpp:81
std::string property_path
Dotted path, e.g. "layout.left".
Definition tweak_store.hpp:82
choc::value::Value value
Definition tweak_store.hpp:83
A design snapshot the drift logic diffs tweaks against.
Definition tweak_store.hpp:300
std::unordered_set< std::string > anchors
Definition tweak_store.hpp:301
std::unordered_map< std::string, std::unordered_set< std::string > > properties
Definition tweak_store.hpp:303
Outcome of a disk operation.
Definition tweak_store.hpp:191
std::size_t bypass_count
Definition tweak_store.hpp:195
std::string path
Definition tweak_store.hpp:196
std::size_t tweak_count
Definition tweak_store.hpp:194
std::string error
Definition tweak_store.hpp:193
bool ok
Definition tweak_store.hpp:192
Three-way classification of every stored tweak against a design.
Definition tweak_store.hpp:277
bool has_drift() const
Definition tweak_store.hpp:285
std::vector< DriftedTweak > drifted
Anchor resolves, property gone.
Definition tweak_store.hpp:279
std::size_t total() const
Definition tweak_store.hpp:282
std::vector< Record > clean
Anchor + property both resolve.
Definition tweak_store.hpp:278
std::vector< DriftedTweak > orphaned
Anchor itself is gone.
Definition tweak_store.hpp:280
One drifted tweak — a stored edit that no longer maps cleanly to the current design.
Definition tweak_store.hpp:268
DriftReason reason
Definition tweak_store.hpp:273
std::string source
Definition tweak_store.hpp:272
std::string property_path
Definition tweak_store.hpp:270
std::string anchor_id
Definition tweak_store.hpp:269
choc::value::Value value
Definition tweak_store.hpp:271
A single tweak record returned by list_tweaks().
Definition tweak_store.hpp:56
std::string property_path
Definition tweak_store.hpp:58
std::string source
Definition tweak_store.hpp:60
choc::value::Value value
Definition tweak_store.hpp:59
std::string anchor_id
Definition tweak_store.hpp:57