Skip to content

Commit 400b0c5

Browse files
authored
Merge PR #24: production semantic map policy
fix(replay): keep production semantic maps local
2 parents bfa7ec4 + 382af7b commit 400b0c5

8 files changed

Lines changed: 83 additions & 60 deletions

packages/tugboat/CHANGELOG.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,14 @@
4242
instead of reusing the previous route's latest frame. Supersession,
4343
lifecycle cancellation, capture failure, and bounded timeout outcomes
4444
complete deterministically without allowing late readbacks to publish.
45+
- **Production viewport semantics stay local** — production sessions can still
46+
build viewport semantic maps for tap resolution, but `viewport_semantic_map`
47+
and `scroll_semantic_snapshot` events are emitted only during exploration so
48+
UI text from semantic nodes is not uploaded in lean production captures.
4549
- **`ExplorationCaptureSink.recordFrame`** — forwards frame metadata and PNG bytes over the
4650
exploration WebSocket instead of dropping them. The CLI persists these under `frames/` when
4751
Flutter capture produces them (local capture may still be suppressed after the socket connects
4852
for performance; see `collector-integration.md`).
49-
50-
### Removed
51-
52-
- **`control_inventory` SDK events** — the SDK no longer emits per-screen control
53-
inventories over the exploration WebSocket. Possible actions should be derived
54-
from screenshots (for example via a VLM) instead of SDK-side widget-tree scans.
55-
State anchors still include `actionableSummary` role counts for fingerprinting
56-
only; that field is not a substitute for control discovery.
57-
58-
### Changed
59-
6053
- **`actionableSummary` deduplication** — count only leaf canonical controls (one per
6154
`FilledButton`/`TextButton`, not nested ink-well chrome). Fixes over-counting in modal,
6255
visibility, and rebuild fingerprint tests.
@@ -78,6 +71,14 @@
7871
`pushNamedAndRemoveUntil` still never bump the route epoch or cancel the pending
7972
destination capture (now covered by a dedicated regression test).
8073

74+
### Removed
75+
76+
- **`control_inventory` SDK events** — the SDK no longer emits per-screen control
77+
inventories over the exploration WebSocket. Possible actions should be derived
78+
from screenshots (for example via a VLM) instead of SDK-side widget-tree scans.
79+
State anchors still include `actionableSummary` role counts for fingerprinting
80+
only; that field is not a substitute for control discovery.
81+
8182
## 0.1.0
8283

8384
- Initial Tugboat Flutter SDK with screenshot evidence, interaction anchors,

packages/tugboat/lib/src/controller.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3210,8 +3210,7 @@ class TugboatReplayController extends ChangeNotifier {
32103210
}) {
32113211
if (config.profile != TugboatCaptureProfile.exploration) return;
32123212
// Always emit raw scene_inventory first (when new). Semantic-map emission
3213-
// must not replace or suppress inventory — it is the local inventory source
3214-
// of truth; maps are a companion / production bridge.
3213+
// must not replace or suppress inventory; maps are an exploration companion.
32153214
final dedupeKey = '${inventory.stateSignature}|${inventory.inventoryHash}';
32163215
if (_emittedInventories.add(dedupeKey)) {
32173216
_addEvent(

packages/tugboat/lib/src/replay_config.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,19 @@ TugboatViewportSemanticPolicy resolveViewportSemanticPolicy({
4949
return TugboatViewportSemanticPolicy.off;
5050
}
5151

52-
final emitEvents =
53-
mode == TugboatViewportSemanticMode.full ||
54-
mode == TugboatViewportSemanticMode.fullWithDebugLogs;
55-
final debugLogs = mode == TugboatViewportSemanticMode.fullWithDebugLogs;
5652
final exploration = profile == TugboatCaptureProfile.exploration;
5753
final production = profile == TugboatCaptureProfile.productionLean;
5854
if (!exploration && !production) {
5955
return TugboatViewportSemanticPolicy.off;
6056
}
6157

58+
final verboseMode =
59+
mode == TugboatViewportSemanticMode.full ||
60+
mode == TugboatViewportSemanticMode.fullWithDebugLogs;
61+
final emitEvents = exploration && verboseMode;
62+
final debugLogs =
63+
exploration && mode == TugboatViewportSemanticMode.fullWithDebugLogs;
64+
6265
return TugboatViewportSemanticPolicy(
6366
engineEnabled: true,
6467
emitEvents: emitEvents,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// Keep this in sync with packages/tugboat/pubspec.yaml. The SDK version test
22
// reads pubspec.yaml directly so release bumps fail fast if this drifts.
3-
const tugboatSdkVersion = '0.4.9';
3+
const tugboatSdkVersion = '0.4.10';

packages/tugboat/lib/src/viewport_semantic_mode.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ enum TugboatViewportSemanticMode {
66
/// Build maps on-device for tap verdicts only; do not emit map events.
77
tapResolutionOnly,
88

9-
/// Build and emit full semantic map / scroll snapshot events.
9+
/// Build and emit full semantic map / scroll snapshot events in exploration.
10+
/// Production uses this as tap-resolution-only to avoid uploading maps.
1011
full,
1112

12-
/// Same as [full], plus debugPrint diagnostics.
13+
/// Same as [full], plus debugPrint diagnostics in exploration.
1314
fullWithDebugLogs,
1415
}

packages/tugboat/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: tugboat
22
description: >-
33
Screenshot-based session replay with compact interaction anchors for Tugboat.
4-
version: 0.4.9
4+
version: 0.4.10
55
repository: https://github.com/blendto/tugboat-flutter
66
issue_tracker: https://github.com/blendto/tugboat-flutter/issues
77
homepage: https://github.com/blendto/tugboat-flutter

packages/tugboat/test/viewport_semantic_map_test.dart

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -391,48 +391,54 @@ void main() {
391391
expect(resolution.keys, isNot(contains('text')));
392392
});
393393

394-
testWidgets('production semantic map requires explicit test opt-in', (
395-
tester,
396-
) async {
397-
await tester.pumpWidget(
398-
MaterialApp(
399-
builder: (context, child) => TugboatReplay.wrapApp(
400-
config: const TugboatReplayConfig(
401-
profile: TugboatCaptureProfile.productionLean,
402-
settleDelay: Duration.zero,
403-
enableGlobalPointerCapture: false,
404-
capturePixelRatio: 1.0,
405-
viewportSemanticMode: TugboatViewportSemanticMode.full,
406-
viewportSemanticMapMaxNodes: 1,
394+
testWidgets(
395+
'production semantic map emission stays off even with full mode',
396+
(tester) async {
397+
await tester.pumpWidget(
398+
MaterialApp(
399+
builder: (context, child) => TugboatReplay.wrapApp(
400+
config: const TugboatReplayConfig(
401+
profile: TugboatCaptureProfile.productionLean,
402+
settleDelay: Duration.zero,
403+
enableGlobalPointerCapture: false,
404+
capturePixelRatio: 1.0,
405+
viewportSemanticMode: TugboatViewportSemanticMode.full,
406+
viewportSemanticMapMaxNodes: 1,
407+
),
408+
child: child!,
407409
),
408-
child: child!,
409-
),
410-
home: Scaffold(
411-
body: Column(
412-
children: [
413-
FilledButton(onPressed: () {}, child: const Text('Go')),
414-
FilledButton(onPressed: () {}, child: const Text('Next')),
415-
],
410+
home: Scaffold(
411+
body: Column(
412+
children: [
413+
FilledButton(onPressed: () {}, child: const Text('Go')),
414+
FilledButton(onPressed: () {}, child: const Text('Next')),
415+
],
416+
),
416417
),
417418
),
418-
),
419-
);
420-
await tester.pump();
421-
await _waitForCaptures(tester);
419+
);
420+
await tester.pump();
421+
await _waitForCaptures(tester);
422422

423-
final controller = TugboatReplay.controller!;
424-
controller.recordPointerDown(tester.getCenter(find.text('Go')));
425-
await tester.pump();
423+
final controller = TugboatReplay.controller!;
424+
controller.recordPointerDown(tester.getCenter(find.text('Go')));
425+
await tester.pump();
426426

427-
final mapEvent = controller.session!.events
428-
.where((event) => event.type == 'viewport_semantic_map')
429-
.single;
430-
final nodes = mapEvent.data['nodes'] as List;
431-
final summary = mapEvent.data['summary'] as Map<Object?, Object?>;
432-
expect(nodes.length, lessThanOrEqualTo(1));
433-
expect(summary['truncatedCount'], isA<int>());
434-
expect(summary['totalNodes'], nodes.length);
435-
});
427+
final mapEvents = controller.session!.events
428+
.where(
429+
(event) =>
430+
event.type == 'viewport_semantic_map' ||
431+
event.type == 'scroll_semantic_snapshot',
432+
)
433+
.toList();
434+
expect(mapEvents, isEmpty);
435+
436+
final tapEvent = controller.session!.events
437+
.where((event) => event.type == 'tap')
438+
.last;
439+
expect(tapEvent.data['viewportSemanticResolution'], isNotNull);
440+
},
441+
);
436442

437443
testWidgets('production semantic map stays off with default mode', (
438444
tester,
@@ -576,7 +582,10 @@ void main() {
576582
.whereType<String>()
577583
.where((fingerprint) => fingerprint.isNotEmpty)
578584
.toSet();
579-
expect(linkedFingerprints.intersection(inventoryFingerprints), isNotEmpty);
585+
expect(
586+
linkedFingerprints.intersection(inventoryFingerprints),
587+
isNotEmpty,
588+
);
580589

581590
final inventoryIndex = events.indexWhere(
582591
(event) => event.type == 'scene_inventory',

packages/tugboat/test/viewport_semantic_policy_test.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,19 @@ void main() {
4747
mode: TugboatViewportSemanticMode.full,
4848
);
4949
expect(productionFull.engineEnabled, isTrue);
50-
expect(productionFull.emitEvents, isTrue);
50+
expect(productionFull.emitEvents, isFalse);
51+
expect(productionFull.debugLogs, isFalse);
5152
expect(productionFull.holdPersistentSemanticsHandle, isFalse);
5253

54+
final productionDebug = resolveViewportSemanticPolicy(
55+
profile: TugboatCaptureProfile.productionLean,
56+
mode: TugboatViewportSemanticMode.fullWithDebugLogs,
57+
);
58+
expect(productionDebug.engineEnabled, isTrue);
59+
expect(productionDebug.emitEvents, isFalse);
60+
expect(productionDebug.debugLogs, isFalse);
61+
expect(productionDebug.holdPersistentSemanticsHandle, isFalse);
62+
5363
final productionDefault = const TugboatReplayConfig(
5464
profile: TugboatCaptureProfile.productionLean,
5565
).viewportSemanticPolicy;

0 commit comments

Comments
 (0)