@trillboards/edge-sdk 0.2.138 → 0.2.150

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 (191) hide show
  1. package/README.md +2 -2
  2. package/deploy/detector/pyproject.toml +20 -2
  3. package/deploy/detector/tests/test_execution.py +124 -0
  4. package/deploy/detector/tests/test_model_identity.py +80 -0
  5. package/deploy/detector/tests/test_registry.py +18 -0
  6. package/deploy/detector/tests/test_server.py +3 -1
  7. package/deploy/detector/tests/test_version_stamp.py +4 -1
  8. package/deploy/detector/trillboards_detector/_version.py +2 -2
  9. package/deploy/detector/trillboards_detector/cli.py +3 -0
  10. package/deploy/detector/trillboards_detector/detections.py +7 -0
  11. package/deploy/detector/trillboards_detector/errors.py +4 -0
  12. package/deploy/detector/trillboards_detector/execution.py +109 -0
  13. package/deploy/detector/trillboards_detector/loader.py +6 -3
  14. package/deploy/detector/trillboards_detector/registry.py +27 -5
  15. package/deploy/detector/trillboards_detector/server.py +29 -2
  16. package/deploy/detector/trillboards_detector/session.py +15 -3
  17. package/deploy/litertlm/test_trillboards_litertlm_shim.py +81 -0
  18. package/deploy/litertlm/test_verify_model_proof.py +30 -0
  19. package/deploy/litertlm/trillboards_litertlm_shim.py +150 -1
  20. package/deploy/litertlm/verify_model_proof.py +30 -0
  21. package/deploy/macos/TrillboardsSetupApp.m +131 -3
  22. package/deploy/macos/build-macos-pkg.sh +13 -10
  23. package/deploy/macos/scripts/setup-detector.sh +2 -2
  24. package/deploy/macos/scripts/setup-litertlm.sh +66 -30
  25. package/deploy/macos/test-setup-litertlm-reinstall.sh +57 -4
  26. package/deploy/setup-guidance.json +112 -0
  27. package/deploy/systemd/setup-detector.sh +2 -2
  28. package/deploy/systemd/setup-litertlm.sh +51 -27
  29. package/deploy/windows/build-windows.ps1 +11 -6
  30. package/deploy/windows/bundle-windows-installer.ps1 +3 -3
  31. package/deploy/windows/e2e/test-local-sense.ps1 +6 -5
  32. package/deploy/windows/install-update.ps1 +52 -9
  33. package/deploy/windows/set-executable-icon.ps1 +24 -5
  34. package/deploy/windows/setup-detector.ps1 +142 -4
  35. package/deploy/windows/setup-litertlm.ps1 +53 -10
  36. package/deploy/windows/trillboards-setup.ps1 +101 -2
  37. package/dist/AudienceAnalyzeClient.d.ts +10 -0
  38. package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
  39. package/dist/AudienceAnalyzeClient.js +5 -4
  40. package/dist/AudienceAnalyzeClient.js.map +1 -1
  41. package/dist/CloudSensingManager.d.ts +38 -1
  42. package/dist/CloudSensingManager.d.ts.map +1 -1
  43. package/dist/CloudSensingManager.js +403 -25
  44. package/dist/CloudSensingManager.js.map +1 -1
  45. package/dist/DetectorFieldProducer.d.ts +5 -1
  46. package/dist/DetectorFieldProducer.d.ts.map +1 -1
  47. package/dist/DetectorFieldProducer.js +8 -1
  48. package/dist/DetectorFieldProducer.js.map +1 -1
  49. package/dist/DurableVlmCandidateSpool.d.ts +83 -2
  50. package/dist/DurableVlmCandidateSpool.d.ts.map +1 -1
  51. package/dist/DurableVlmCandidateSpool.js +596 -30
  52. package/dist/DurableVlmCandidateSpool.js.map +1 -1
  53. package/dist/EdgeAgent.js +1 -1
  54. package/dist/FrameGroupingShadow.d.ts +51 -0
  55. package/dist/FrameGroupingShadow.d.ts.map +1 -0
  56. package/dist/FrameGroupingShadow.js +158 -0
  57. package/dist/FrameGroupingShadow.js.map +1 -0
  58. package/dist/ImageClassifierFieldProducer.d.ts +36 -0
  59. package/dist/ImageClassifierFieldProducer.d.ts.map +1 -0
  60. package/dist/ImageClassifierFieldProducer.js +150 -0
  61. package/dist/ImageClassifierFieldProducer.js.map +1 -0
  62. package/dist/LiteRtVlmProvider.d.ts +5 -0
  63. package/dist/LiteRtVlmProvider.d.ts.map +1 -1
  64. package/dist/LiteRtVlmProvider.js +72 -7
  65. package/dist/LiteRtVlmProvider.js.map +1 -1
  66. package/dist/LocalDetectorClient.d.ts +6 -0
  67. package/dist/LocalDetectorClient.d.ts.map +1 -1
  68. package/dist/LocalDetectorClient.js +8 -0
  69. package/dist/LocalDetectorClient.js.map +1 -1
  70. package/dist/OrderedFrameReview.d.ts +57 -0
  71. package/dist/OrderedFrameReview.d.ts.map +1 -0
  72. package/dist/OrderedFrameReview.js +121 -0
  73. package/dist/OrderedFrameReview.js.map +1 -0
  74. package/dist/OrderedReviewClient.d.ts +38 -0
  75. package/dist/OrderedReviewClient.d.ts.map +1 -0
  76. package/dist/OrderedReviewClient.js +121 -0
  77. package/dist/OrderedReviewClient.js.map +1 -0
  78. package/dist/OrderedSequenceStore.d.ts +69 -0
  79. package/dist/OrderedSequenceStore.d.ts.map +1 -0
  80. package/dist/OrderedSequenceStore.js +257 -0
  81. package/dist/OrderedSequenceStore.js.map +1 -0
  82. package/dist/ProfileReviewWork.d.ts +68 -0
  83. package/dist/ProfileReviewWork.d.ts.map +1 -0
  84. package/dist/ProfileReviewWork.js +150 -0
  85. package/dist/ProfileReviewWork.js.map +1 -0
  86. package/dist/RetainedFieldContract.d.ts +21 -0
  87. package/dist/RetainedFieldContract.d.ts.map +1 -0
  88. package/dist/RetainedFieldContract.js +92 -0
  89. package/dist/RetainedFieldContract.js.map +1 -0
  90. package/dist/ReviewModelUse.d.ts +7 -0
  91. package/dist/ReviewModelUse.d.ts.map +1 -0
  92. package/dist/ReviewModelUse.js +30 -0
  93. package/dist/ReviewModelUse.js.map +1 -0
  94. package/dist/ReviewPriority.d.ts +12 -0
  95. package/dist/ReviewPriority.d.ts.map +1 -0
  96. package/dist/ReviewPriority.js +45 -0
  97. package/dist/ReviewPriority.js.map +1 -0
  98. package/dist/RuntimeModelSelection.d.ts +10 -0
  99. package/dist/RuntimeModelSelection.d.ts.map +1 -0
  100. package/dist/RuntimeModelSelection.js +58 -0
  101. package/dist/RuntimeModelSelection.js.map +1 -0
  102. package/dist/SceneChangeDetector.d.ts +1 -1
  103. package/dist/SceneChangeDetector.d.ts.map +1 -1
  104. package/dist/SceneChangeDetector.js +18 -6
  105. package/dist/SceneChangeDetector.js.map +1 -1
  106. package/dist/ScopedVlmReview.d.ts +76 -0
  107. package/dist/ScopedVlmReview.d.ts.map +1 -0
  108. package/dist/ScopedVlmReview.js +267 -0
  109. package/dist/ScopedVlmReview.js.map +1 -0
  110. package/dist/SensingProfileClient.d.ts +50 -0
  111. package/dist/SensingProfileClient.d.ts.map +1 -1
  112. package/dist/SensingProfileClient.js +58 -2
  113. package/dist/SensingProfileClient.js.map +1 -1
  114. package/dist/TemporalReviewClient.d.ts +43 -0
  115. package/dist/TemporalReviewClient.d.ts.map +1 -0
  116. package/dist/TemporalReviewClient.js +117 -0
  117. package/dist/TemporalReviewClient.js.map +1 -0
  118. package/dist/TemporalReviewJournal.d.ts +91 -0
  119. package/dist/TemporalReviewJournal.d.ts.map +1 -0
  120. package/dist/TemporalReviewJournal.js +337 -0
  121. package/dist/TemporalReviewJournal.js.map +1 -0
  122. package/dist/UrgentReviewTriage.d.ts +35 -0
  123. package/dist/UrgentReviewTriage.d.ts.map +1 -0
  124. package/dist/UrgentReviewTriage.js +52 -0
  125. package/dist/UrgentReviewTriage.js.map +1 -0
  126. package/dist/VideoSegmentUploader.d.ts +293 -0
  127. package/dist/VideoSegmentUploader.d.ts.map +1 -0
  128. package/dist/VideoSegmentUploader.js +1002 -0
  129. package/dist/VideoSegmentUploader.js.map +1 -0
  130. package/dist/VlmCandidateBuffer.d.ts +19 -0
  131. package/dist/VlmCandidateBuffer.d.ts.map +1 -1
  132. package/dist/VlmCandidateBuffer.js.map +1 -1
  133. package/dist/autoSense.d.ts +2 -2
  134. package/dist/autoSense.d.ts.map +1 -1
  135. package/dist/autoSense.js +16 -11
  136. package/dist/autoSense.js.map +1 -1
  137. package/dist/cli.js +1 -1
  138. package/dist/cli.js.map +1 -1
  139. package/dist/config.d.ts +2 -0
  140. package/dist/config.d.ts.map +1 -1
  141. package/dist/config.js.map +1 -1
  142. package/dist/continuousReview.d.ts +71 -0
  143. package/dist/continuousReview.d.ts.map +1 -0
  144. package/dist/continuousReview.js +53 -0
  145. package/dist/continuousReview.js.map +1 -0
  146. package/dist/detectorCountReview.d.ts +9 -0
  147. package/dist/detectorCountReview.d.ts.map +1 -0
  148. package/dist/detectorCountReview.js +79 -0
  149. package/dist/detectorCountReview.js.map +1 -0
  150. package/dist/diagnose.d.ts +1 -1
  151. package/dist/diagnose.js +1 -1
  152. package/dist/installerOnboardingMode.d.ts.map +1 -1
  153. package/dist/installerOnboardingMode.js +5 -3
  154. package/dist/installerOnboardingMode.js.map +1 -1
  155. package/dist/localInferenceSchema.d.ts +8 -0
  156. package/dist/localInferenceSchema.d.ts.map +1 -1
  157. package/dist/localInferenceSchema.js +3 -3
  158. package/dist/localInferenceSchema.js.map +1 -1
  159. package/dist/localSenseE2eHarness.d.ts +12 -0
  160. package/dist/localSenseE2eHarness.d.ts.map +1 -1
  161. package/dist/localSenseE2eHarness.js +77 -4
  162. package/dist/localSenseE2eHarness.js.map +1 -1
  163. package/dist/localSensing.d.ts +91 -18
  164. package/dist/localSensing.d.ts.map +1 -1
  165. package/dist/localSensing.js +678 -153
  166. package/dist/localSensing.js.map +1 -1
  167. package/dist/mixedScopedCounts.d.ts +17 -0
  168. package/dist/mixedScopedCounts.d.ts.map +1 -0
  169. package/dist/mixedScopedCounts.js +90 -0
  170. package/dist/mixedScopedCounts.js.map +1 -0
  171. package/dist/sensingCatchRate.d.ts +9 -5
  172. package/dist/sensingCatchRate.d.ts.map +1 -1
  173. package/dist/sensingCatchRate.js +10 -6
  174. package/dist/sensingCatchRate.js.map +1 -1
  175. package/dist/sensingRunMode.d.ts +5 -0
  176. package/dist/sensingRunMode.d.ts.map +1 -1
  177. package/dist/sensingRunMode.js +56 -0
  178. package/dist/sensingRunMode.js.map +1 -1
  179. package/package.json +7 -4
  180. package/retained/ArchiveLifecycle.cjs +79 -0
  181. package/retained/ModelStageScheduler.cjs +66 -0
  182. package/retained/ProfileAuthorityClient.cjs +88 -0
  183. package/retained/RetainedCaptureClient.cjs +164 -0
  184. package/retained/WaitRecheckCaptureClient.cjs +109 -0
  185. package/retained/__tests__/archive-lifecycle.test.cjs +117 -0
  186. package/retained/__tests__/runtime.test.cjs +384 -0
  187. package/retained/alert-rule-policy.cjs +37 -0
  188. package/retained/manager.cjs +133 -0
  189. package/retained/pilot-profile-client.cjs +82 -0
  190. package/retained/temporal-grouping.cjs +78 -0
  191. package/retained/website-api-capability.cjs +122 -0
@@ -40,6 +40,14 @@ static BOOL acquireSetupInstanceLock(void) {
40
40
 
41
41
  @interface TrillboardsSetupDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate, WKNavigationDelegate, WKUIDelegate>
42
42
  @property(nonatomic, strong) NSWindow *window;
43
+ @property(nonatomic, strong) NSDictionary *guidanceCatalog;
44
+ @property(nonatomic, strong) NSView *guidancePanel;
45
+ @property(nonatomic, strong) NSTextField *guidanceTitle;
46
+ @property(nonatomic, strong) NSTextField *guidanceBody;
47
+ @property(nonatomic, strong) NSButton *guidanceToggle;
48
+ @property(nonatomic, strong) NSButton *guidanceRestore;
49
+ @property(nonatomic, strong) NSMutableSet<NSString *> *dismissedGuidance;
50
+
43
51
  @property(nonatomic, strong) NSFileHandle *setupOutput;
44
52
  @property(nonatomic, strong) NSMutableData *setupOutputBuffer;
45
53
  @property(nonatomic, strong) NSWindow *cameraWindow;
@@ -327,14 +335,14 @@ static BOOL acquireSetupInstanceLock(void) {
327
335
 
328
336
  - (void)buildWindow {
329
337
  self.window = [[NSWindow alloc]
330
- initWithContentRect:NSMakeRect(0, 0, 760, 540)
338
+ initWithContentRect:NSMakeRect(0, 0, 760, 700)
331
339
  styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable
332
340
  backing:NSBackingStoreBuffered
333
341
  defer:NO];
334
342
  self.window.title = @"Sense Anything Connector";
335
343
  self.window.delegate = self;
336
- self.window.minSize = NSMakeSize(760, 540);
337
- self.window.maxSize = NSMakeSize(760, 540);
344
+ self.window.minSize = NSMakeSize(760, 700);
345
+ self.window.maxSize = NSMakeSize(760, 700);
338
346
  [self.window center];
339
347
 
340
348
  NSView *root = self.window.contentView;
@@ -536,10 +544,85 @@ static BOOL acquireSetupInstanceLock(void) {
536
544
  [self.chooseAccountPanel addSubview:self.chooseAccountSubmit];
537
545
  [root addSubview:self.chooseAccountPanel];
538
546
 
547
+ // Reserve a separate help region; never cover native forms or recovery actions.
548
+ for (NSView *view in root.subviews) {
549
+ NSRect frame = view.frame;
550
+ frame.origin.y += 160;
551
+ view.frame = frame;
552
+ }
553
+ NSView *guideSidebar = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 190, 160)];
554
+ guideSidebar.wantsLayer = YES;
555
+ guideSidebar.layer.backgroundColor = NSColor.controlBackgroundColor.CGColor;
556
+ [root addSubview:guideSidebar];
557
+ NSString *guidePath = [NSBundle.mainBundle pathForResource:@"setup-guidance" ofType:@"json"];
558
+ NSData *guideData = guidePath ? [NSData dataWithContentsOfFile:guidePath] : nil;
559
+ id catalog = guideData ? [NSJSONSerialization JSONObjectWithData:guideData options:0 error:nil] : nil;
560
+ self.guidanceCatalog = [catalog isKindOfClass:NSDictionary.class] ? catalog : @{};
561
+ self.dismissedGuidance = [NSMutableSet set];
562
+ self.guidanceToggle = [NSButton checkboxWithTitle:@"Guiding tips" target:self action:@selector(toggleGuidance)];
563
+ self.guidanceToggle.frame = NSMakeRect(24, 118, 150, 24);
564
+ self.guidanceToggle.state = [NSUserDefaults.standardUserDefaults boolForKey:@"setupGuidingTipsDisabled"] ? NSControlStateValueOff : NSControlStateValueOn;
565
+ [root addSubview:self.guidanceToggle];
566
+ self.guidanceRestore = [self panelButton:@"Show this tip" frame:NSMakeRect(20, 80, 150, 28) action:@selector(restoreGuidance)];
567
+ [root addSubview:self.guidanceRestore];
568
+ self.guidancePanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 12, 465, 145)];
569
+ self.guidancePanel.wantsLayer = YES;
570
+ self.guidancePanel.layer.backgroundColor = [NSColor colorWithCalibratedRed:1 green:0.96 blue:0.91 alpha:1].CGColor;
571
+ self.guidancePanel.layer.borderColor = [NSColor colorWithCalibratedRed:0.93 green:0.65 blue:0.40 alpha:1].CGColor;
572
+ self.guidancePanel.layer.borderWidth = 1;
573
+ self.guidancePanel.layer.cornerRadius = 10;
574
+ self.guidanceTitle = [self label:@"" size:13 weight:NSFontWeightSemibold frame:NSMakeRect(14, 94, 405, 40)];
575
+ self.guidanceBody = [self label:@"" size:12 weight:NSFontWeightRegular frame:NSMakeRect(14, 35, 435, 60)];
576
+ self.guidanceTitle.textColor = [NSColor colorWithCalibratedRed:0.28 green:0.20 blue:0.14 alpha:1];
577
+ self.guidanceBody.textColor = [NSColor colorWithCalibratedRed:0.38 green:0.29 blue:0.23 alpha:1];
578
+ [self.guidancePanel addSubview:self.guidanceTitle];
579
+ [self.guidancePanel addSubview:self.guidanceBody];
580
+ NSButton *dismiss = [self panelButton:@"×" frame:NSMakeRect(424, 110, 30, 26) action:@selector(dismissGuidance)];
581
+ dismiss.accessibilityLabel = @"Dismiss this tip";
582
+ [self.guidancePanel addSubview:dismiss];
583
+ [self.guidancePanel addSubview:[self panelButton:@"More help" frame:NSMakeRect(8, 5, 110, 26) action:@selector(showGuidanceDetail)]];
584
+ [root addSubview:self.guidancePanel];
585
+
539
586
  self.onboardingPanels = @[self.accountPanel, self.termsPanel, self.planPanel, self.chooseAccountPanel];
540
587
  [self showOnboardingPanel:nil];
541
588
  }
542
589
 
590
+ - (void)renderGuidance {
591
+ NSDictionary *guide = self.guidanceCatalog[self.lastStage];
592
+ BOOL enabled = self.guidanceToggle.state == NSControlStateValueOn;
593
+ BOOL dismissed = [self.dismissedGuidance containsObject:self.lastStage];
594
+ self.guidancePanel.hidden = !enabled || dismissed || ![guide isKindOfClass:NSDictionary.class];
595
+ self.guidanceRestore.hidden = !enabled || !dismissed;
596
+ self.guidanceTitle.stringValue = [guide[@"title"] isKindOfClass:NSString.class] ? guide[@"title"] : @"";
597
+ self.guidanceBody.stringValue = [guide[@"body"] isKindOfClass:NSString.class] ? guide[@"body"] : @"";
598
+ }
599
+
600
+ - (void)toggleGuidance {
601
+ [NSUserDefaults.standardUserDefaults setBool:self.guidanceToggle.state != NSControlStateValueOn forKey:@"setupGuidingTipsDisabled"];
602
+ [self.dismissedGuidance removeAllObjects];
603
+ [self renderGuidance];
604
+ }
605
+
606
+ - (void)dismissGuidance {
607
+ [self.dismissedGuidance addObject:self.lastStage];
608
+ [self renderGuidance];
609
+ }
610
+
611
+ - (void)restoreGuidance {
612
+ [self.dismissedGuidance removeObject:self.lastStage];
613
+ [self renderGuidance];
614
+ }
615
+
616
+ - (void)showGuidanceDetail {
617
+ NSDictionary *guide = self.guidanceCatalog[self.lastStage];
618
+ if (![guide[@"detail"] isKindOfClass:NSString.class]) return;
619
+ NSAlert *alert = [[NSAlert alloc] init];
620
+ alert.messageText = self.guidanceTitle.stringValue;
621
+ alert.informativeText = guide[@"detail"];
622
+ [alert addButtonWithTitle:@"Got it"];
623
+ [alert beginSheetModalForWindow:self.window completionHandler:nil];
624
+ }
625
+
543
626
  - (void)showOnboardingPanel:(NSView *)panel {
544
627
  for (NSView *candidate in self.onboardingPanels ?: @[]) candidate.hidden = candidate != panel;
545
628
  BOOL showStatus = panel == nil;
@@ -663,6 +746,7 @@ static BOOL acquireSetupInstanceLock(void) {
663
746
  - (void)showWelcome {
664
747
  [self showOnboardingPanel:nil];
665
748
  self.lastStage = @"welcome";
749
+ [self renderGuidance];
666
750
  self.headline.stringValue = @"Set up this computer for CCTV";
667
751
  self.detail.stringValue = @"Connect your Sense Anything account, find your recorder, and verify every camera before screens are created.";
668
752
  self.statusValue.stringValue = @"About 5 minutes";
@@ -678,6 +762,7 @@ static BOOL acquireSetupInstanceLock(void) {
678
762
  - (void)showLocalError:(NSString *)title message:(NSString *)message {
679
763
  [self showOnboardingPanel:nil];
680
764
  self.lastStage = @"error";
765
+ [self renderGuidance];
681
766
  self.headline.stringValue = title;
682
767
  self.detail.stringValue = message;
683
768
  self.statusValue.stringValue = @"Setup stopped";
@@ -763,6 +848,7 @@ static BOOL acquireSetupInstanceLock(void) {
763
848
  }
764
849
  self.requiredUpdateURL = downloadURL;
765
850
  self.lastStage = @"update_required";
851
+ [self renderGuidance];
766
852
  self.headline.stringValue = @"Update required before setup can continue";
767
853
  self.detail.stringValue = @"Install the latest Sense Anything Connector, then reopen it to finish camera setup.";
768
854
  self.statusValue.stringValue = [NSString stringWithFormat:@"Version %@", availableText];
@@ -835,6 +921,7 @@ static BOOL acquireSetupInstanceLock(void) {
835
921
  if (![files fileExistsAtPath:logPath]) [files createFileAtPath:logPath contents:nil attributes:nil];
836
922
 
837
923
  self.lastStage = @"starting";
924
+ [self renderGuidance];
838
925
  self.setupStarted = YES;
839
926
  self.headline.stringValue = @"Starting camera setup";
840
927
  self.detail.stringValue = @"Connecting securely to Sense Anything.";
@@ -868,6 +955,7 @@ static BOOL acquireSetupInstanceLock(void) {
868
955
  if (!stage) return;
869
956
  NSString *previousStage = self.lastStage;
870
957
  self.lastStage = stage;
958
+ [self renderGuidance];
871
959
  if ([state[@"portalUrl"] isKindOfClass:NSString.class]) self.portalURL = state[@"portalUrl"];
872
960
  if ([state[@"title"] isKindOfClass:NSString.class]) self.headline.stringValue = state[@"title"];
873
961
  if ([state[@"message"] isKindOfClass:NSString.class]) self.detail.stringValue = state[@"message"];
@@ -1168,6 +1256,43 @@ static BOOL acquireSetupInstanceLock(void) {
1168
1256
 
1169
1257
  @end
1170
1258
 
1259
+ static int runGuidanceSmokeTest(void) {
1260
+ [NSApplication sharedApplication];
1261
+ TrillboardsSetupDelegate *delegate = [[TrillboardsSetupDelegate alloc] init];
1262
+ [delegate buildWindow];
1263
+ if (delegate.guidanceCatalog.count == 0) return 1;
1264
+ delegate.guidanceToggle.state = NSControlStateValueOn;
1265
+ for (NSString *stage in delegate.guidanceCatalog) {
1266
+ delegate.lastStage = stage;
1267
+ [delegate renderGuidance];
1268
+ if (delegate.guidancePanel.hidden || !delegate.guidanceTitle.stringValue.length) return 2;
1269
+ for (NSTextField *field in @[delegate.guidanceTitle, delegate.guidanceBody]) {
1270
+ NSRect textBounds = [field.attributedStringValue boundingRectWithSize:NSMakeSize(field.frame.size.width - 4, CGFLOAT_MAX)
1271
+ options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading];
1272
+ if (textBounds.size.height > field.frame.size.height) {
1273
+ fprintf(stderr, "Guidance text clipped at %s\n", stage.UTF8String);
1274
+ return 3;
1275
+ }
1276
+ }
1277
+ [delegate dismissGuidance];
1278
+ if (!delegate.guidancePanel.hidden || delegate.guidanceRestore.hidden) return 4;
1279
+ [delegate restoreGuidance];
1280
+ if (delegate.guidancePanel.hidden) return 5;
1281
+ }
1282
+ delegate.lastStage = @"onboarding_terms";
1283
+ delegate.termsCheck.state = NSControlStateValueOff;
1284
+ delegate.termsSubmit.enabled = NO;
1285
+ delegate.guidanceToggle.state = NSControlStateValueOff;
1286
+ [delegate renderGuidance];
1287
+ if (!delegate.guidancePanel.hidden || delegate.termsSubmit.enabled) return 6;
1288
+ if (NSIntersectsRect(delegate.guidancePanel.frame, delegate.accountPanel.frame) ||
1289
+ NSIntersectsRect(delegate.guidancePanel.frame, delegate.actionButton.frame)) return 7;
1290
+ fprintf(stdout, "Native guidance stages, dismissal, text bounds and consent isolation passed.\n");
1291
+ fflush(stdout);
1292
+ [delegate.window close];
1293
+ return 0;
1294
+ }
1295
+
1171
1296
  static int runCancellationSmokeTest(void) {
1172
1297
  NSString *pidPath = [NSTemporaryDirectory()
1173
1298
  stringByAppendingPathComponent:[NSString stringWithFormat:@"trillboards-setup-child-%@.pid", NSUUID.UUID.UUIDString]];
@@ -1288,6 +1413,9 @@ int main(int argc, const char *argv[]) {
1288
1413
  if (argc == 3 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-ui-pipe"]) {
1289
1414
  return runNativePipeSmokeTest([NSString stringWithUTF8String:argv[2]]);
1290
1415
  }
1416
+ if (argc == 2 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-guidance"]) {
1417
+ return runGuidanceSmokeTest();
1418
+ }
1291
1419
  if (argc == 2 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-cancel"]) {
1292
1420
  return runCancellationSmokeTest();
1293
1421
  }
@@ -16,12 +16,11 @@
16
16
  # lib/edge-sdk/dist/cli.js the connector CLI
17
17
  # lib/node_modules/... the runtime subset (edge-core +
18
18
  # edge-platform-rtsp + edge-sdk + their
19
- # transitive deps), WITHOUT the heavy local
20
- # ONNX/sharp inference stack.
19
+ # transitive deps), including Sharp for scoped
20
+ # evidence masks, without the ONNX stack.
21
21
  #
22
- # Because cloud-sense + pair are loaded WITHOUT the local-inference stack
23
- # (cli.ts lazy-requires EdgeAgent), the bundle omits onnxruntime-node/sharp/
24
- # better-sqlite3 — keeping the pkg small.
22
+ # The local CCTV path does not load EdgeAgent's ONNX stack. The bundle omits
23
+ # onnxruntime-node/better-sqlite3, but retains Sharp and its native pixel codec.
25
24
  #
26
25
  # Install (no sudo, no admin password):
27
26
  # installer -pkg <pkg> -target CurrentUserHomeDirectory
@@ -131,8 +130,8 @@ FFMPEG_SHA256_X64="${FFMPEG_SHA256_X64:-cfe20936c83ecf5d68e424b87e8cc45b24dd6be8
131
130
  # at a verified local cache; otherwise the release build fetches the pinned URL.
132
131
  # Override GEMMA_MODEL_URL / GEMMA_MODEL_SHA256 only when upstream is re-pinned.
133
132
  GEMMA_MODEL_ID="gemma-4-E2B-it.litertlm"
134
- GEMMA_MODEL_URL="${GEMMA_MODEL_URL:-https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm}"
135
- GEMMA_MODEL_SHA256="${GEMMA_MODEL_SHA256:-181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c}"
133
+ GEMMA_MODEL_URL="${GEMMA_MODEL_URL:-https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/84b6978eff6e4eea02825bc2ee4ea48579f13109/gemma-4-E2B-it.litertlm}"
134
+ GEMMA_MODEL_SHA256="${GEMMA_MODEL_SHA256:-ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42}"
136
135
  GEMMA_MODEL_FILE="${GEMMA_MODEL_FILE:-}"
137
136
  BUNDLE_MODEL=1
138
137
 
@@ -207,6 +206,7 @@ log "Building all edge workspaces (tsc, dependency order)…"
207
206
  log "Staging edge-sdk dist…"
208
207
  mkdir -p "$PAYLOAD/lib/edge-sdk"
209
208
  cp -R "$PKG_ROOT/dist" "$PAYLOAD/lib/edge-sdk/dist"
209
+ cp -R "$PKG_ROOT/retained" "$PAYLOAD/lib/edge-sdk/retained"
210
210
  cp "$PKG_ROOT/package.json" "$PAYLOAD/lib/edge-sdk/package.json"
211
211
 
212
212
  # ─── 3. Lay down the runtime node_modules subset (NO heavy native stack) ──────
@@ -214,12 +214,10 @@ cp "$PKG_ROOT/package.json" "$PAYLOAD/lib/edge-sdk/package.json"
214
214
  # closure) and EXCLUDE the heavy/native/dev packages. cloud-sense + pair never
215
215
  # require them (EdgeAgent → edge-sensing → onnxruntime-node is lazy now), so
216
216
  # their absence is fine and keeps the pkg ~50 MB instead of ~270 MB.
217
- log "Staging runtime node_modules (excluding ONNX/sharp/sqlite/dev)…"
217
+ log "Staging runtime node_modules (including focused-image masks; excluding ONNX/sqlite/dev)…"
218
218
  mkdir -p "$PAYLOAD/lib/node_modules"
219
219
  rsync -a \
220
220
  --exclude 'onnxruntime-node' \
221
- --exclude 'sharp' \
222
- --exclude '@img' \
223
221
  --exclude 'better-sqlite3' \
224
222
  --exclude 'ffmpeg-static' \
225
223
  --exclude '.cache' \
@@ -244,6 +242,7 @@ for p in edge-core edge-sensing edge-ads edge-federated edge-cloud \
244
242
  rm -rf "$dest"
245
243
  mkdir -p "$dest"
246
244
  [ -d "$src/dist" ] && cp -R "$src/dist" "$dest/dist"
245
+ [ "$p" != "edge-sdk" ] || cp -R "$src/retained" "$dest/retained"
247
246
  cp "$src/package.json" "$dest/package.json"
248
247
  done
249
248
 
@@ -424,6 +423,7 @@ cp "$PKG_ROOT/deploy/macos/scripts/setup-litertlm.sh" "$PAYLOAD/share/setup-lite
424
423
  # looks first. It is copied into the litert-lm tool venv's site-packages at
425
424
  # provisioning time; nothing else reads it, and no upstream file is modified.
426
425
  cp "$PKG_ROOT/deploy/litertlm/trillboards_litertlm_shim.py" "$PAYLOAD/share/trillboards_litertlm_shim.py"
426
+ cp "$PKG_ROOT/deploy/litertlm/verify_model_proof.py" "$PAYLOAD/share/verify_model_proof.py"
427
427
  cp "$PKG_ROOT/deploy/macos/scripts/ensure-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh"
428
428
  cp "$PKG_ROOT/deploy/macos/apply-update.sh" "$PAYLOAD/share/apply-update.sh"
429
429
 
@@ -490,6 +490,7 @@ SETUP_APP_BIN="$SETUP_APP_MACOS/Sense Anything Connector"
490
490
  mkdir -p "$SETUP_APP_MACOS" "$SETUP_APP_RESOURCES"
491
491
  cp "$PKG_ROOT/deploy/branding/SenseAnythingConnector.icns" \
492
492
  "$SETUP_APP_RESOURCES/SenseAnythingConnector.icns"
493
+ cp "$PKG_ROOT/deploy/setup-guidance.json" "$SETUP_APP_RESOURCES/setup-guidance.json"
493
494
  log "Building native Sense Anything Connector.app…"
494
495
  if [ "$UNIVERSAL" -eq 1 ]; then
495
496
  clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch arm64 -arch x86_64 -framework Cocoa -framework WebKit \
@@ -536,6 +537,8 @@ cat > "$SETUP_APP/Contents/Info.plist" <<PLIST
536
537
  </plist>
537
538
  PLIST
538
539
  plutil -lint "$SETUP_APP/Contents/Info.plist" >/dev/null || die "native setup app Info.plist is invalid"
540
+ "$SETUP_APP_BIN" --smoke-test-guidance \
541
+ || die "native setup guidance smoke test failed"
539
542
  file "$SETUP_APP_BIN" | grep -q 'Mach-O' || die "native setup app executable is not Mach-O"
540
543
  log "Native setup app staged: $SETUP_APP"
541
544
 
@@ -146,7 +146,7 @@ fi
146
146
  # nothing — an exact pin cannot resolve to different bytes.
147
147
  SOURCE="$(find_detector_source)" || { log "ERROR: detector package source not found next to this script"; exit 0; }
148
148
  log "installing/repairing trillboards-detector from $SOURCE with uv-managed Python $DETECTOR_PYTHON"
149
- if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "$SOURCE" >>"$LOG" 2>&1; then
149
+ if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "${SOURCE}[cpu]" >>"$LOG" 2>&1; then
150
150
  log "ERROR: trillboards-detector install failed; the cascade stays off (the VLM still runs every frame)"
151
151
  exit 0
152
152
  fi
@@ -182,7 +182,7 @@ elif [ "$HAVE_VERSION" != "$WANT_VERSION" ]; then
182
182
  # asking it again the same way is not a repair.
183
183
  log "WARN: the installed detector reports '$HAVE_VERSION' but the staged package is '$WANT_VERSION' - reinstalling from scratch"
184
184
  "$UV" tool uninstall trillboards-detector >>"$LOG" 2>&1 || true
185
- "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "$SOURCE" >>"$LOG" 2>&1 || true
185
+ "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "${SOURCE}[cpu]" >>"$LOG" 2>&1 || true
186
186
  DETECTOR="$LOCAL_BIN/trillboards-detector"
187
187
  [ -x "$DETECTOR" ] || DETECTOR="$(command -v trillboards-detector || echo "$DETECTOR")"
188
188
  HAVE_VERSION="$(installed_detector_version)"
@@ -29,6 +29,9 @@ LA_DIR="$USER_HOME/Library/LaunchAgents"
29
29
  LOCAL_BIN="$USER_HOME/.local/bin"
30
30
  MODEL_ID="gemma-4-E2B-it.litertlm"
31
31
  HF_REPO="litert-community/gemma-4-E2B-it-litert-lm"
32
+ MODEL_REVISION="84b6978eff6e4eea02825bc2ee4ea48579f13109"
33
+ APPROVED_MODEL_SHA256="ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42"
34
+ MODEL_URL="https://huggingface.co/$HF_REPO/resolve/$MODEL_REVISION/$MODEL_ID"
32
35
  SERVE_LABEL="com.trillboards.litertlm"
33
36
  SERVE_PORT="9379"
34
37
  LITERT_PYTHON="${LITERT_LM_PYTHON:-3.12}"
@@ -245,16 +248,29 @@ install_litertlm_shim() {
245
248
  # Deliberately NOT a /v1/models check: that answers 200 without ever building an
246
249
  # engine, so it cannot tell a working GPU encoder from one that fails on first
247
250
  # use.
248
- verify_litertlm_vision() {
249
- _body="$LITERT_DIR/.vision-probe.json"
250
- printf '{"model":"%s,gpu","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
251
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
252
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
251
+ verify_litertlm_probe() {
252
+ local requested="$1" body response code tool_dir py verifier candidate valid=1
253
+ body="$LITERT_DIR/.identity-probe.json"
254
+ response="$LITERT_DIR/.identity-response.json"
255
+ printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
256
+ "$requested" "$LITERTLM_PROBE_JPEG_B64" > "$body" 2>/dev/null || return 1
257
+ code=$(curl -s -o "$response" -w '%{http_code}' --max-time 180 \
253
258
  -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
254
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
255
- rm -f "$_body" 2>/dev/null || true
256
- [ "$_code" = "200" ]
259
+ -H 'Content-Type: application/json' -H "X-Trillboards-Model-Sha256: $APPROVED_MODEL_SHA256" \
260
+ --data-binary @"$body" 2>/dev/null)
261
+ if [ "$code" = "200" ]; then
262
+ tool_dir="$("${UV:-$LOCAL_BIN/uv}" tool dir 2>/dev/null | tr -d '\r')"
263
+ py="$tool_dir/litert-lm/bin/python"
264
+ for candidate in "$SCRIPT_DIR/verify_model_proof.py" "$SCRIPT_DIR/../litertlm/verify_model_proof.py" "$SCRIPT_DIR/../../litertlm/verify_model_proof.py"; do
265
+ [ -f "$candidate" ] && { verifier="$candidate"; break; }
266
+ done
267
+ [ -n "${verifier:-}" ] && [ -x "$py" ] \
268
+ && "$py" "$verifier" "$response" "$requested" "$APPROVED_MODEL_SHA256" && valid=0
269
+ fi
270
+ rm -f "$body" "$response" 2>/dev/null || true
271
+ return "$valid"
257
272
  }
273
+ verify_litertlm_vision() { verify_litertlm_probe "$MODEL_ID,gpu"; }
258
274
 
259
275
  # The same probe, but asking for NO delegate suffix rather than `,gpu`.
260
276
  #
@@ -265,16 +281,7 @@ verify_litertlm_vision() {
265
281
  # carries no suffix at all, so a 200 means the box built an engine and ran a real
266
282
  # image through it under the config now on disk, which is exactly the claim each
267
283
  # rung needs.
268
- verify_litertlm_plain_inference() {
269
- _body="$LITERT_DIR/.plain-probe.json"
270
- printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
271
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
272
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
273
- -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
274
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
275
- rm -f "$_body" 2>/dev/null || true
276
- [ "$_code" = "200" ]
277
- }
284
+ verify_litertlm_plain_inference() { verify_litertlm_probe "$MODEL_ID"; }
278
285
 
279
286
  local_ai_healthy() {
280
287
  curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
@@ -334,11 +341,33 @@ verify_bundled_model() {
334
341
  [ -f "$BUNDLED_MODEL_SHA" ] || fail "bundled model checksum is missing"
335
342
  EXPECTED_SHA="$(awk 'NR == 1 { print $1 }' "$BUNDLED_MODEL_SHA" 2>/dev/null || echo '')"
336
343
  ACTUAL_SHA="$(shasum -a 256 "$BUNDLED_MODEL" 2>/dev/null | awk '{print $1}')"
337
- if [ -z "$EXPECTED_SHA" ] || [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
344
+ if [ "$EXPECTED_SHA" != "$APPROVED_MODEL_SHA256" ] || [ "$ACTUAL_SHA" != "$APPROVED_MODEL_SHA256" ]; then
338
345
  fail "bundled model checksum mismatch"
339
346
  fi
340
347
  log "verified bundled model SHA-256"
341
348
  }
349
+ installed_model_verified() {
350
+ [ -f "$STORE_DIR/model.litertlm" ] || return 1
351
+ [ "$(shasum -a 256 "$STORE_DIR/model.litertlm" 2>/dev/null | awk '{print $1}')" = "$APPROVED_MODEL_SHA256" ]
352
+ }
353
+ import_verified_model() {
354
+ local source="$1" old_sha rollback
355
+ [ "$(shasum -a 256 "$source" 2>/dev/null | awk '{print $1}')" = "$APPROVED_MODEL_SHA256" ] || return 1
356
+ if [ -f "$STORE_DIR/model.litertlm" ] && ! installed_model_verified; then
357
+ old_sha="$(shasum -a 256 "$STORE_DIR/model.litertlm" | awk '{print $1}')"
358
+ rollback="$STORE_DIR/rollback/$old_sha.litertlm"
359
+ mkdir -p "$STORE_DIR/rollback" || return 1
360
+ [ -f "$rollback" ] || cp "$STORE_DIR/model.litertlm" "$rollback" || return 1
361
+ [ "$(shasum -a 256 "$rollback" | awk '{print $1}')" = "$old_sha" ] || return 1
362
+ if launch_agent_loaded; then
363
+ launchctl bootout "gui/$(id -u)/$SERVE_LABEL" >>"$LOG" 2>&1 || return 1
364
+ wait_for_launch_agent_removal || return 1
365
+ fi
366
+ log "previous model preserved at $rollback"
367
+ fi
368
+ "$LITERTLM" import "$source" "$MODEL_ID" >>"$LOG" 2>&1 || return 1
369
+ installed_model_verified
370
+ }
342
371
 
343
372
  # One provisioning process per user. Re-opening the pairing helper is a normal
344
373
  # recovery path, so duplicate launches must collapse without duplicate downloads.
@@ -351,7 +380,7 @@ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
351
380
  log "LiteRT-LM provisioning is already running (pid $LOCK_PID)"
352
381
  if [ "$REQUIRE_BUNDLED_MODEL" = "1" ]; then
353
382
  for _attempt in $(seq 1 120); do
354
- local_ai_healthy && exit 0
383
+ local_ai_healthy && installed_model_verified && (verify_litertlm_vision || verify_litertlm_plain_inference) && exit 0
355
384
  sleep 2
356
385
  done
357
386
  fail "the existing local AI installation did not become healthy"
@@ -468,12 +497,12 @@ fi
468
497
 
469
498
  install_litertlm_shim || true
470
499
 
471
- # 3. Model - import the verified bundled file. Idempotent: skip entirely if
472
- # litert-lm already has the model. Engineering runs may use HF only with the
500
+ # 3. Model - skip only if the installed BYTES match the approved artifact.
501
+ # Engineering runs may use the immutable HF revision only with the
473
502
  # explicit TRILLBOARDS_ALLOW_MODEL_DOWNLOAD=1 opt-in.
474
503
  HAVE_MODEL=0
475
- if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
476
- log "model $MODEL_ID already present — skipping import"
504
+ if installed_model_verified; then
505
+ log "approved model $MODEL_ID already present — skipping import"
477
506
  HAVE_MODEL=1
478
507
  if [ -f "$BUNDLED_MODEL" ]; then
479
508
  verify_bundled_model
@@ -483,7 +512,7 @@ if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
483
512
  elif [ -f "$BUNDLED_MODEL" ]; then
484
513
  verify_bundled_model
485
514
  log "importing $MODEL_ID from BUNDLED file $BUNDLED_MODEL (no download) …"
486
- if "$LITERTLM" import "$BUNDLED_MODEL" "$MODEL_ID" >>"$LOG" 2>&1; then
515
+ if import_verified_model "$BUNDLED_MODEL"; then
487
516
  log "model imported from bundle"
488
517
  HAVE_MODEL=1
489
518
  # Reclaim the staged ~2.4 GB — the litert-lm store now holds the model.
@@ -497,12 +526,19 @@ fi
497
526
  if [ "$HAVE_MODEL" -eq 0 ]; then
498
527
  [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "required bundled model is missing"
499
528
  [ "$ALLOW_MODEL_DOWNLOAD" = "1" ] || fail "local AI model is missing and network fallback is disabled"
500
- log "importing $MODEL_ID from $HF_REPO (~2.4 GB, public repo) …"
501
- "$LITERTLM" import --from-huggingface-repo "$HF_REPO" "$MODEL_ID" >>"$LOG" 2>&1 \
502
- && { log "model imported from HF"; HAVE_MODEL=1; } \
503
- || log "WARN: model import failed (serve will retry on next run)"
529
+ mkdir -p "$STORE_DIR" || fail "model store is unavailable"
530
+ DOWNLOAD="$(mktemp "$STORE_DIR/approved-model.XXXXXX")" || fail "could not create a model download file"
531
+ log "downloading approved $MODEL_ID from immutable revision $MODEL_REVISION"
532
+ if curl -fL --retry 3 --output "$DOWNLOAD" "$MODEL_URL" >>"$LOG" 2>&1 && import_verified_model "$DOWNLOAD"; then
533
+ HAVE_MODEL=1
534
+ rm -f "$DOWNLOAD"
535
+ log "model imported from verified immutable revision"
536
+ else
537
+ rm -f "$DOWNLOAD"
538
+ fail "approved model download/import failed"
539
+ fi
504
540
  fi
505
- [ "$HAVE_MODEL" -eq 1 ] || { [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI model is unavailable"; }
541
+ [ "$HAVE_MODEL" -eq 1 ] || fail "local AI model is unavailable"
506
542
 
507
543
  # Record the installed version next to the store model so the connector's
508
544
  # edge-core ModelManager seeds it (readBundledModelVersion) and does not
@@ -10,9 +10,29 @@ export HOME="$TEST_ROOT/home"
10
10
  export TRILLBOARDS_TEST_STATE_DIR="$TEST_ROOT/state"
11
11
  export LITERT_LM_DIR="$TEST_ROOT/store"
12
12
  export TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1
13
+ export TRILLBOARDS_TEST_REAL_PYTHON="${TRILLBOARDS_TEST_REAL_PYTHON:-$(command -v python3)}"
13
14
  FAKE_BIN="$HOME/.local/bin"
14
15
  SHARE_DIR="$HOME/Library/Trillboards/edge/share"
15
16
  mkdir -p "$FAKE_BIN" "$SHARE_DIR" "$TRILLBOARDS_TEST_STATE_DIR"
17
+ mkdir -p "$TRILLBOARDS_TEST_STATE_DIR/tools/litert-lm/bin" "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm"
18
+ printf 'fixture evaluated model\n' >"$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm"
19
+
20
+ # These are tiny installer fixtures, not model-inference/accuracy tests. Hashes
21
+ # are simulated from file CONTENT, so the same name with old bytes is rejected.
22
+ cat >"$FAKE_BIN/shasum" <<'EOF'
23
+ #!/bin/bash
24
+ file="${@: -1}"
25
+ if [ -f "$file" ] && [ "$(cat "$file")" = 'fixture evaluated model' ]; then
26
+ printf 'ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42 %s\n' "$file"
27
+ else
28
+ if [ -x /usr/bin/shasum ]; then /usr/bin/shasum "$@"; else /usr/bin/sha256sum "$file"; fi
29
+ fi
30
+ EOF
31
+ cat >"$TRILLBOARDS_TEST_STATE_DIR/tools/litert-lm/bin/python" <<'EOF'
32
+ #!/bin/bash
33
+ [ "${1:-}" != '-c' ] || exit 0
34
+ exec "$TRILLBOARDS_TEST_REAL_PYTHON" "$@"
35
+ EOF
16
36
 
17
37
  cat >"$FAKE_BIN/uname" <<'EOF'
18
38
  #!/bin/bash
@@ -25,14 +45,20 @@ EOF
25
45
 
26
46
  cat >"$FAKE_BIN/uv" <<'EOF'
27
47
  #!/bin/bash
48
+ [ "${1:-} ${2:-}" != 'tool dir' ] || printf '%s\n' "$TRILLBOARDS_TEST_STATE_DIR/tools"
28
49
  exit 0
29
50
  EOF
30
51
 
31
52
  cat >"$FAKE_BIN/litert-lm" <<'EOF'
32
53
  #!/bin/bash
33
- if [ "${1:-}" = "list" ]; then
34
- printf 'gemma-4-E2B-it.litertlm\n'
35
- fi
54
+ case "${1:-}" in
55
+ list) printf 'gemma-4-E2B-it.litertlm\n' ;;
56
+ --version) printf 'litert-lm 0.16.0\n' ;;
57
+ import)
58
+ cp "$2" "$LITERT_LM_DIR/models/$3/model.litertlm"
59
+ printf 'import\n' >>"$TRILLBOARDS_TEST_STATE_DIR/imports"
60
+ ;;
61
+ esac
36
62
  exit 0
37
63
  EOF
38
64
 
@@ -54,15 +80,21 @@ EOF
54
80
  cat >"$FAKE_BIN/curl" <<'EOF'
55
81
  #!/bin/bash
56
82
  body=""
83
+ response=""
57
84
  prev=""
58
85
  for arg in "$@"; do
59
86
  case "$prev" in
60
87
  --data-binary) body="${arg#@}" ;;
88
+ -o) response="$arg" ;;
61
89
  esac
62
90
  prev="$arg"
63
91
  done
64
92
  for arg in "$@"; do
65
93
  if [ "$arg" = "-w" ]; then
94
+ model="$(sed -n 's/.*"model":"\([^"]*\)".*/\1/p' "$body")"
95
+ case "$model" in *,gpu|*,cpu) ;; *) model="$model,cpu" ;; esac
96
+ sha="${TRILLBOARDS_TEST_PROOF_SHA:-ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42}"
97
+ printf '{"runtimeModel":{"version":1,"format":"litert-lm","sha256":"%s","runtimeModelId":"%s"}}' "$sha" "$model" >"$response"
66
98
  if [ "${TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE:-0}" = "1" ] \
67
99
  && grep -Fq '"speculative_decoding":true' "$LITERT_LM_DIR/config.json" 2>/dev/null; then
68
100
  printf '500'
@@ -127,7 +159,8 @@ exit 0
127
159
  EOF
128
160
 
129
161
  chmod +x "$FAKE_BIN/uname" "$FAKE_BIN/uv" "$FAKE_BIN/litert-lm" \
130
- "$FAKE_BIN/curl" "$FAKE_BIN/sleep" "$FAKE_BIN/launchctl"
162
+ "$FAKE_BIN/curl" "$FAKE_BIN/sleep" "$FAKE_BIN/launchctl" "$FAKE_BIN/shasum" \
163
+ "$TRILLBOARDS_TEST_STATE_DIR/tools/litert-lm/bin/python"
131
164
 
132
165
  cat >"$SHARE_DIR/com.trillboards.litertlm.plist" <<'EOF'
133
166
  <plist><string>__LITERTLM__</string><string>__PORT__</string><string>__LOG_DIR__</string></plist>
@@ -247,5 +280,25 @@ fi
247
280
 
248
281
  unset TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE
249
282
 
283
+ # An existing model under the SAME id must be replaced when its bytes differ,
284
+ # with a checked rollback copy. The fixture importer performs a real file copy.
285
+ printf 'fixture old model\n' >"$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm"
286
+ old_sha="$("$FAKE_BIN/shasum" -a 256 "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm" | awk '{print $1}')"
287
+ mkdir -p "$SHARE_DIR/models"
288
+ printf 'fixture evaluated model\n' >"$SHARE_DIR/models/gemma-4-E2B-it.litertlm"
289
+ printf 'ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42\n' >"$SHARE_DIR/models/gemma-4-E2B-it.litertlm.sha256"
290
+ if ! OUTPUT4="$(/bin/bash "$SETUP" 2>&1)"; then printf '%s\n' "$OUTPUT4" >&2; exit 1; fi
291
+ [ "$(cat "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm")" = 'fixture evaluated model' ]
292
+ [ "$("$FAKE_BIN/shasum" -a 256 "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/rollback/$old_sha.litertlm" | awk '{print $1}')" = "$old_sha" ]
293
+ [ "$(wc -l <"$TRILLBOARDS_TEST_STATE_DIR/imports" | tr -d ' ')" = '1' ]
294
+ # A correct store and a 200 inference must still fail when the loaded engine's
295
+ # identity differs. This catches a stale in-memory model after an upgrade.
296
+ export TRILLBOARDS_TEST_PROOF_SHA="181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c"
297
+ if OUTPUT5="$(/bin/bash "$SETUP" 2>&1)"; then
298
+ printf 'FAIL: setup accepted a stale runtime model proof\n' >&2
299
+ exit 1
300
+ fi
301
+ unset TRILLBOARDS_TEST_PROOF_SHA
302
+
250
303
  printf 'macOS LiteRT-LM reinstall race + engine-settings cascade regressions passed\n'
251
304