@hasna/recordings 0.3.2 → 0.3.9

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 (59) hide show
  1. package/README.md +38 -34
  2. package/bun.lock +96 -36
  3. package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
  4. package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
  5. package/dist/cli/index.js +1059 -219
  6. package/dist/cli/macos-shortcut.d.ts +2 -2
  7. package/dist/db/pg-migrations.d.ts +1 -1
  8. package/dist/http/client.d.ts +6 -14
  9. package/dist/http/client.d.ts.map +1 -1
  10. package/dist/index.js +1971 -175
  11. package/dist/lib/capture-probe.d.ts +3 -3
  12. package/dist/lib/capture-probe.d.ts.map +1 -1
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/macos-bundle.d.ts +1 -1
  15. package/dist/lib/release-install-policy.d.ts +21 -0
  16. package/dist/lib/release-install-policy.d.ts.map +1 -1
  17. package/dist/mcp/index.js +978 -142
  18. package/dist/sdk/index.d.ts +2 -2
  19. package/dist/sdk/index.js +7 -3
  20. package/dist/sdk/v1.generated.d.ts.map +1 -1
  21. package/dist/server/cloud-config.d.ts +4 -19
  22. package/dist/server/cloud-config.d.ts.map +1 -1
  23. package/dist/server/cloud.d.ts +1 -1
  24. package/dist/server/cloud.d.ts.map +1 -1
  25. package/dist/server/index.js +217 -299
  26. package/dist/server/serve.d.ts.map +1 -1
  27. package/dist/storage.d.ts +1 -1
  28. package/dist/storage.d.ts.map +1 -1
  29. package/dist/storage.js +1881 -90
  30. package/dist/store.d.ts.map +1 -1
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.d.ts.map +1 -1
  33. package/package.json +4 -3
  34. package/packaging/macos/build_release_pkg.sh +10 -4
  35. package/packaging/macos/managed_bootstrap.sh +4 -4
  36. package/packaging/macos/scripts/postinstall +2 -2
  37. package/packaging/macos/scripts/preinstall +2 -0
  38. package/scripts/generate-sdk.ts +17 -16
  39. package/scripts/install_macos_app.sh +22 -22
  40. package/scripts/macos_artifact.ts +48 -39
  41. package/scripts/migrate.ts +2 -2
  42. package/scripts/release-suite-gate.ts +174 -0
  43. package/scripts/set-version.ts +5 -5
  44. package/scripts/smoke_macos_app.sh +21 -21
  45. package/scripts/vacuity-manifests/version-sites.tsv +8 -4
  46. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  47. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
  48. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  49. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  50. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  51. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  52. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  53. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  54. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  55. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  56. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  57. package/src/native/Recordings/build.sh +14 -20
  58. package/dist/lib/retired-deployment-modes.d.ts +0 -24
  59. package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
@@ -34,11 +34,11 @@ async function main() {
34
34
  getCloudPg,
35
35
  migrateCloudSchema,
36
36
  pingCloudConnectivity,
37
- resolveCloudDatabaseUrl,
37
+ resolveDatabaseUrl,
38
38
  } = await import("../src/server/cloud.js");
39
39
  const { assertCloudSchemaContract } = await import("../src/server/cloud-readiness.js");
40
40
  const { runCloudMigration } = await import("../src/server/migrate-command.js");
41
- const url = resolveCloudDatabaseUrl();
41
+ const url = resolveDatabaseUrl();
42
42
  if (!url) {
43
43
  console.error(
44
44
  "migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)",
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Release-suite gate (wired into `prepublishOnly`).
4
+ *
5
+ * Runs the app's own Linux-CI-partitioned suite — `test:gated`, which covers
6
+ * every discovered `*.test.ts` (the quarantine file is empty by design) — and
7
+ * fails on ANY failure.
8
+ *
9
+ * The allowlist is SELF-INVALIDATING, the same property as
10
+ * `.github/linux-quarantine.txt`: if an allowlisted failure stops failing
11
+ * (the owning lane lands its fix, or a quarantined package ages out of the
12
+ * 7-day window), the gate exits 1 and demands the entry be removed. Any
13
+ * failure outside the allowlist fails the gate.
14
+ *
15
+ * Allowlist history (both entries removed 2026-08-23 by the 0.3.9 release
16
+ * lane, publish-all-recordings, per this gate's own self-invalidation
17
+ * contract — each failure stopped failing at head 15b6181c6):
18
+ * - service-contract-manifest.test.ts (credential_seam_compliance): the
19
+ * contracts-surface migration landed in hasna/apps PR #957
20
+ * (resolveStorageClient now imported from @hasna/contracts/client), so
21
+ * the conformance check passes; the entry's own reason said "Remove this
22
+ * entry when the lane lands."
23
+ * - native-app-companion-contract.test.ts (release supply-chain policy):
24
+ * the companion build's --minimum-release-age=604800 quarantine no longer
25
+ * bites because @hasna/contracts is on the fleet minimumReleaseAgeExcludes
26
+ * list (the sanctioned mechanism for fresh publishes); the test passes.
27
+ * Both removals are the gate's designed behaviour, not a weakening: any
28
+ * future failure now fails the gate with zero allowlisted exceptions.
29
+ *
30
+ * The gate FAILS CLOSED on abnormal termination: a non-zero suite exit is
31
+ * acceptable only when it is fully explained by allowlisted failures — the
32
+ * runner completed (its summary line is present), every parsed failure is
33
+ * allowlisted, at least one allowlisted failure fired, and the runner's
34
+ * reported failure count equals the parsed count. A crash, signal kill,
35
+ * timeout, or runner error that produces a non-zero exit without a complete
36
+ * summary fails the gate (remediation cycle 2 of PR hasna/apps#826).
37
+ */
38
+ import { spawnSync } from "node:child_process";
39
+ import { mkdtempSync, writeFileSync } from "node:fs";
40
+ import { tmpdir } from "node:os";
41
+ import { join } from "node:path";
42
+
43
+ const SEP = "::";
44
+
45
+ const GATED_KEYS: Array<{ file: string; test: string; owner: string; reason: string }> = [];
46
+
47
+ function run(argv: string[], env: Record<string, string>): { status: number; stdout: string; stderr: string } {
48
+ const res = spawnSync(process.execPath, argv, { encoding: "utf8", env: { ...process.env, ...env } });
49
+ return { status: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
50
+ }
51
+
52
+ // 1. Validate the partition (quarantine file must be consistent).
53
+ const check = run(["scripts/ci-linux-suite.ts", "--check"], {});
54
+ if (check.status !== 0) {
55
+ console.error("release-suite-gate: partition check failed:\n" + (check.stderr || check.stdout));
56
+ process.exit(1);
57
+ }
58
+
59
+ // 2. Collect the gated file list.
60
+ const gated = run(["scripts/ci-linux-suite.ts", "--gated"], {});
61
+ if (gated.status !== 0) {
62
+ console.error("release-suite-gate: could not enumerate gated files:\n" + (gated.stderr || gated.stdout));
63
+ process.exit(1);
64
+ }
65
+ const gatedFiles = gated.stdout.split("\n").filter(Boolean);
66
+ if (gatedFiles.length === 0) {
67
+ console.error("release-suite-gate: zero gated files - refusing to pass on an empty suite.");
68
+ process.exit(1);
69
+ }
70
+
71
+ // 3. Run the gated suite exactly like `test:gated`.
72
+ const logDir = mkdtempSync(join(tmpdir(), "recordings-release-gate-"));
73
+ const logPath = join(logDir, "suite.log");
74
+ const suite = spawnSync(
75
+ process.execPath,
76
+ ["test", "--timeout", "120000", ...gatedFiles],
77
+ {
78
+ encoding: "utf8",
79
+ env: { ...process.env, RECORDINGS_TEST_TIMEOUT_MS: "120000" },
80
+ maxBuffer: 64 * 1024 * 1024,
81
+ },
82
+ );
83
+ const output = (suite.stdout ?? "") + (suite.stderr ?? "");
84
+ try {
85
+ writeFileSync(logPath, output);
86
+ } catch {
87
+ /* log capture is best-effort */
88
+ }
89
+ const suiteStatus = suite.status ?? -1;
90
+
91
+ // 4. Parse failing tests as (file, description) pairs, in output order.
92
+ const failPairs: Array<{ file: string; test: string }> = [];
93
+ let currentFile = "";
94
+ for (const line of output.split("\n")) {
95
+ const fileMatch = /^(?:\.\/)?(src\/[^\s:]+\.test\.ts):$/.exec(line);
96
+ if (fileMatch) {
97
+ currentFile = fileMatch[1];
98
+ continue;
99
+ }
100
+ const failMatch = /^\(fail\) (.+) \[\d+.*\]$/.exec(line);
101
+ if (failMatch) {
102
+ failPairs.push({ file: currentFile, test: failMatch[1].trim() });
103
+ }
104
+ }
105
+
106
+ // 5. Classify.
107
+ const failuresByKey = new Map<string, number>();
108
+ for (const key of GATED_KEYS) {
109
+ failuresByKey.set(key.file + SEP + key.test, 0);
110
+ }
111
+ const unexpected: Array<{ file: string; test: string }> = [];
112
+ for (const pair of failPairs) {
113
+ const key = pair.file + SEP + pair.test;
114
+ if (failuresByKey.has(key)) {
115
+ failuresByKey.set(key, (failuresByKey.get(key) ?? 0) + 1);
116
+ } else {
117
+ unexpected.push(pair);
118
+ }
119
+ }
120
+
121
+ if (unexpected.length > 0) {
122
+ console.error(
123
+ "release-suite-gate: FAIL - unexpected test failures:\n" +
124
+ unexpected.map((u) => " " + u.file + " :: " + u.test).join("\n"),
125
+ );
126
+ console.error("Full suite log: " + logPath);
127
+ process.exit(1);
128
+ }
129
+
130
+ // 6. Fail closed on abnormal termination. A non-zero suite exit is only
131
+ // acceptable when it is fully explained by the allowlisted failures: the
132
+ // runner completed (summary line present), every parsed failure is
133
+ // allowlisted, at least one allowlisted failure fired, and the runner's
134
+ // reported failure count equals the parsed count (no unparsed failures).
135
+ // A crash, signal kill, timeout, or runner error produces a non-zero exit
136
+ // without a complete summary and must fail the gate.
137
+ if (suiteStatus !== 0) {
138
+ const summaryMatch = /Ran (\d+) tests across (\d+) files/.exec(output);
139
+ // The tally is "N pass[,/ ] M fail" in the runner's closing summary. The
140
+ // LAST match is taken (bun prints the tally once, but a naive first match
141
+ // can hit "0.13.3 failed to resolve" -> "3 fail" inside a version string).
142
+ const tallyMatches = [...output.matchAll(/(\d+) pass[\s,/]+(\d+) fail/g)];
143
+ const failCountMatch = tallyMatches.length > 0 ? tallyMatches[tallyMatches.length - 1] : null;
144
+ const reportedFails = failCountMatch ? Number(failCountMatch[2]) : null;
145
+ const anyAllowlistedFired = failPairs.length > 0;
146
+ const complete = summaryMatch !== null && reportedFails !== null && reportedFails === failPairs.length;
147
+ if (!anyAllowlistedFired || !complete) {
148
+ console.error(
149
+ "release-suite-gate: FAIL - suite exited " + suiteStatus +
150
+ " with " + failPairs.length + " parsed failure(s) but the run is incomplete or unexplained:" +
151
+ "\n summary line present: " + (summaryMatch !== null) +
152
+ "\n reported failure count: " + String(reportedFails) +
153
+ "\n parsed failure count: " + failPairs.length,
154
+ );
155
+ console.error("Full suite log: " + logPath);
156
+ process.exit(1);
157
+ }
158
+ }
159
+
160
+ // 7. Self-invalidation: an allowlisted failure that no longer fails must be removed.
161
+ const stale = GATED_KEYS.filter((key) => (failuresByKey.get(key.file + SEP + key.test) ?? 0) === 0);
162
+ if (stale.length > 0) {
163
+ console.error(
164
+ "release-suite-gate: FAIL - allowlisted failure no longer fails; remove the entry from scripts/release-suite-gate.ts:\n" +
165
+ stale.map((s) => " " + s.file + " :: " + s.test + " - " + s.owner).join("\n"),
166
+ );
167
+ process.exit(1);
168
+ }
169
+
170
+ console.log(
171
+ "release-suite-gate: PASS - " + failPairs.length + " classified pre-existing failure(s) allowlisted:" +
172
+ failPairs.map((p) => "\n " + p.file + " :: " + p.test).join(""),
173
+ );
174
+ process.exit(0);
@@ -16,17 +16,17 @@ export type VersionSite = {
16
16
  pattern: RegExp;
17
17
  };
18
18
 
19
+ // src/version.ts is deliberately NOT a site: it derives VERSION from
20
+ // package.json at import time (see src/version.ts), so it holds no literal
21
+ // this writer could keep in sync, and a monorepo changesets bump can no
22
+ // longer leave it stale. The remaining sites are the native bundle surfaces,
23
+ // which cannot derive and must be written here.
19
24
  export const versionSites: VersionSite[] = [
20
25
  {
21
26
  file: "package.json",
22
27
  label: "package.json version",
23
28
  pattern: /("version"\s*:\s*")([^"]+)(")/,
24
29
  },
25
- {
26
- file: "src/version.ts",
27
- label: "src/version.ts VERSION",
28
- pattern: /(export const VERSION = ")([^"]+)(")/,
29
- },
30
30
  {
31
31
  file: "src/native/Recordings/RecordingsLib/Info.plist",
32
32
  label: "Info.plist CFBundleShortVersionString",
@@ -9,7 +9,7 @@ if [ "$#" -ge 3 ]; then
9
9
  SMOKE_VARIANT="${4:-full}"
10
10
  ;;
11
11
  *)
12
- echo "Usage: $0 <Recordings.app> <explicit absolute Bun executable path> [--variant full|bar]" >&2
12
+ echo "Usage: $0 <HasnaRecordings.app> <explicit absolute Bun executable path> [--variant full|bar]" >&2
13
13
  exit 2
14
14
  ;;
15
15
  esac
@@ -51,7 +51,7 @@ HOST_PLATFORM="$("$HOST_UNAME_EXECUTABLE" -s)"
51
51
  readonly HOST_PLATFORM
52
52
 
53
53
  if [ "$HOST_PLATFORM" != "Darwin" ] && [ "${RECORDINGS_TEST_SMOKE_ALLOW_NON_DARWIN:-0}" != "1" ]; then
54
- echo "Recordings.app runtime smoke is only supported on macOS." >&2
54
+ echo "HasnaRecordings.app runtime smoke is only supported on macOS." >&2
55
55
  exit 1
56
56
  fi
57
57
 
@@ -123,12 +123,12 @@ HOME_DIRECTORY="${HOME:-}"
123
123
  case "$HOME_DIRECTORY" in
124
124
  /*) ;;
125
125
  *)
126
- echo "Recordings.app runtime smoke requires an absolute HOME path." >&2
126
+ echo "HasnaRecordings.app runtime smoke requires an absolute HOME path." >&2
127
127
  exit 1
128
128
  ;;
129
129
  esac
130
130
  if [ ! -d "$HOME_DIRECTORY" ]; then
131
- echo "Recordings.app runtime smoke HOME is not a directory: $HOME_DIRECTORY" >&2
131
+ echo "HasnaRecordings.app runtime smoke HOME is not a directory: $HOME_DIRECTORY" >&2
132
132
  exit 1
133
133
  fi
134
134
 
@@ -136,7 +136,7 @@ APP_PARENT="$(cd -P "$("$DIRNAME_EXECUTABLE" "$1")" && "$PWD_EXECUTABLE" -P)"
136
136
  APP_PATH="$APP_PARENT/$("$BASENAME_EXECUTABLE" "$1")"
137
137
  EXECUTABLE="$APP_PATH/Contents/MacOS/Recordings"
138
138
  if [ ! -x "$EXECUTABLE" ]; then
139
- echo "Recordings.app runtime smoke executable is missing: $EXECUTABLE" >&2
139
+ echo "HasnaRecordings.app runtime smoke executable is missing: $EXECUTABLE" >&2
140
140
  exit 1
141
141
  fi
142
142
 
@@ -317,7 +317,7 @@ cleanup() {
317
317
  fi
318
318
  if [ -n "$SMOKE_APP_PID" ] && [ -n "$SMOKE_APP_PID_START_IDENTITY" ]; then
319
319
  terminate_verified_process \
320
- "Recordings.app" \
320
+ "HasnaRecordings.app" \
321
321
  "$SMOKE_APP_PID" \
322
322
  "$EXECUTABLE" \
323
323
  "$SMOKE_APP_PID_START_IDENTITY" || true
@@ -377,7 +377,7 @@ run_smoke() {
377
377
  if ! SMOKE_APP_IDENTITIES_BEFORE_LAUNCH="$(
378
378
  process_records_for_exact_executable "$EXECUTABLE"
379
379
  )"; then
380
- echo "Recordings.app runtime smoke ${mode} could not inventory exact app processes before launch." >&2
380
+ echo "HasnaRecordings.app runtime smoke ${mode} could not inventory exact app processes before launch." >&2
381
381
  return 1
382
382
  fi
383
383
 
@@ -399,24 +399,24 @@ run_smoke() {
399
399
  result_pid="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
400
400
  "$BUN_EXECUTABLE" -e 'const value = await Bun.file(process.argv[1]).json(); console.log(value.processIdentifier)' "$output")"
401
401
  if ! [[ "$result_pid" =~ ^[1-9][0-9]*$ ]] || ! kill -0 "$result_pid" 2>/dev/null; then
402
- echo "Recordings.app runtime smoke ${mode} reported a process that is not running." >&2
402
+ echo "HasnaRecordings.app runtime smoke ${mode} reported a process that is not running." >&2
403
403
  return 1
404
404
  fi
405
405
  if ! result_pid_start_identity="$(
406
406
  capture_process_start_identity "$result_pid" "$EXECUTABLE"
407
407
  )"; then
408
- echo "Recordings.app runtime smoke ${mode} reported a process outside the exact app path." >&2
408
+ echo "HasnaRecordings.app runtime smoke ${mode} reported a process outside the exact app path." >&2
409
409
  return 1
410
410
  fi
411
411
  if process_identity_existed_before_launch "$result_pid" "$result_pid_start_identity"; then
412
- echo "Recordings.app runtime smoke ${mode} reported an app process that predated this launch." >&2
412
+ echo "HasnaRecordings.app runtime smoke ${mode} reported an app process that predated this launch." >&2
413
413
  return 1
414
414
  fi
415
415
  if [ -n "$SMOKE_APP_PID" ] && {
416
416
  [ "$result_pid" != "$SMOKE_APP_PID" ] ||
417
417
  [ "$result_pid_start_identity" != "$SMOKE_APP_PID_START_IDENTITY" ];
418
418
  }; then
419
- echo "Recordings.app runtime smoke ${mode} evidence did not match the exact launched app process." >&2
419
+ echo "HasnaRecordings.app runtime smoke ${mode} evidence did not match the exact launched app process." >&2
420
420
  return 1
421
421
  fi
422
422
  SMOKE_APP_PID="$result_pid"
@@ -429,19 +429,19 @@ run_smoke() {
429
429
  break
430
430
  fi
431
431
  if ! kill -0 "$SMOKE_PID" 2>/dev/null; then
432
- echo "Recordings.app runtime smoke ${mode} exited without evidence." >&2
432
+ echo "HasnaRecordings.app runtime smoke ${mode} exited without evidence." >&2
433
433
  "$SED_EXECUTABLE" -n '1,120p' "$log" >&2
434
434
  return 1
435
435
  fi
436
436
  "$SLEEP_EXECUTABLE" 0.1
437
437
  done
438
438
  if [ ! -f "$output" ]; then
439
- echo "Recordings.app runtime smoke ${mode} timed out." >&2
439
+ echo "HasnaRecordings.app runtime smoke ${mode} timed out." >&2
440
440
  "$SED_EXECUTABLE" -n '1,120p' "$log" >&2
441
441
  return 1
442
442
  fi
443
443
  if [ -z "$SMOKE_APP_PID" ]; then
444
- echo "Recordings.app runtime smoke ${mode} did not bind evidence to the exact app process." >&2
444
+ echo "HasnaRecordings.app runtime smoke ${mode} did not bind evidence to the exact app process." >&2
445
445
  return 1
446
446
  fi
447
447
 
@@ -546,7 +546,7 @@ run_smoke() {
546
546
  ' "$output"
547
547
 
548
548
  if [ -e "$completion" ]; then
549
- echo "Recordings.app runtime smoke ${mode} produced completion before the challenge." >&2
549
+ echo "HasnaRecordings.app runtime smoke ${mode} produced completion before the challenge." >&2
550
550
  return 1
551
551
  fi
552
552
  challenge="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
@@ -554,7 +554,7 @@ run_smoke() {
554
554
  if ! rechecked_start_identity="$(
555
555
  capture_process_start_identity "$result_pid" "$EXECUTABLE"
556
556
  )" || [ "$rechecked_start_identity" != "$result_pid_start_identity" ]; then
557
- echo "Recordings.app runtime smoke ${mode} process identity changed before completion challenge." >&2
557
+ echo "HasnaRecordings.app runtime smoke ${mode} process identity changed before completion challenge." >&2
558
558
  return 1
559
559
  fi
560
560
  write_atomic_text_file "$acknowledgement" "$challenge"
@@ -564,7 +564,7 @@ run_smoke() {
564
564
  "$SLEEP_EXECUTABLE" 0.1
565
565
  done
566
566
  if kill -0 "$SMOKE_PID" 2>/dev/null; then
567
- echo "Recordings.app runtime smoke ${mode} completion handshake timed out." >&2
567
+ echo "HasnaRecordings.app runtime smoke ${mode} completion handshake timed out." >&2
568
568
  return 1
569
569
  fi
570
570
  if wait "$SMOKE_PID" 2>/dev/null; then
@@ -577,7 +577,7 @@ run_smoke() {
577
577
  SMOKE_APP_PID=""
578
578
  SMOKE_APP_PID_START_IDENTITY=""
579
579
  if [ "$wrapper_status" -ne 0 ]; then
580
- echo "Recordings.app runtime smoke ${mode} open -W wrapper exited unsuccessfully (${wrapper_status})." >&2
580
+ echo "HasnaRecordings.app runtime smoke ${mode} open -W wrapper exited unsuccessfully (${wrapper_status})." >&2
581
581
  "$SED_EXECUTABLE" -n '1,120p' "$log" >&2
582
582
  return 1
583
583
  fi
@@ -602,7 +602,7 @@ run_smoke() {
602
602
  process.exit(1);
603
603
  }
604
604
  ' "$completion"; then
605
- echo "Recordings.app runtime smoke ${mode} did not provide a valid completion response." >&2
605
+ echo "HasnaRecordings.app runtime smoke ${mode} did not provide a valid completion response." >&2
606
606
  return 1
607
607
  fi
608
608
 
@@ -613,7 +613,7 @@ run_smoke normal
613
613
  run_smoke permission-helper
614
614
  run_smoke resolver
615
615
  if [ "$SMOKE_VARIANT" = "bar" ]; then
616
- echo "Recordings.app runtime smoke passed (bar variant): menu bar, windowless launch, helper isolation, and packaged resolver."
616
+ echo "HasnaRecordings.app runtime smoke passed (bar variant): menu bar, windowless launch, helper isolation, and packaged resolver."
617
617
  else
618
- echo "Recordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
618
+ echo "HasnaRecordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
619
619
  fi
@@ -15,9 +15,13 @@
15
15
  # manifest is not wired into a package.json aggregate script.
16
16
  #
17
17
  # --- every version site agrees with package.json. Models 8de7068: one site bumped, the rest not.
18
- duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/version.ts .15 1
19
- duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.2.15 1
20
- duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.2.15 2
18
+ # I38-00553 (2026-08-21): src/version.ts now DERIVES from package.json and holds no literal, so it
19
+ # is no longer a writer site (see set-version.ts) and cannot be mutated here. The residual drift
20
+ # class is the mirror direction: the package.json authority corrupted while the native sites stay
21
+ # put. Rows below were re-needled to the 0.3.5 tree.
22
+ delete src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares package.json 0.3.5 1
23
+ duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.3.5 1
24
+ duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.3.5 2
21
25
  # --- a stale site is reported, not silently accepted: the mismatch filter removed.
22
26
  delete src/__tests__/version-site-guard.test.ts a single stale site is reported rather than silently accepted scripts/set-version.ts .filter((reading) => reading.version !== expected) 1
23
27
  # --- applyVersion writes every site in one pass: the per-file grouping removed.
@@ -58,4 +62,4 @@ delete src/__tests__/version-site-guard.test.ts applyVersion rewrites every site
58
62
  #
59
63
  # The generated SDK's version stamp -- a fifth copy of the release version that `versionSites`
60
64
  # deliberately does not write -- goes stale again.
61
- delete src/__tests__/version-site-guard.test.ts the generated SDK header carries the version package.json declares src/sdk/v1.generated.ts .15 1
65
+ delete src/__tests__/version-site-guard.test.ts the generated SDK header carries the version package.json declares src/sdk/v1.generated.ts 0.3.5 1
@@ -11,17 +11,17 @@
11
11
  <key>CFBundleName</key>
12
12
  <string>Recordings</string>
13
13
  <key>CFBundleDisplayName</key>
14
- <string>Recordings</string>
14
+ <string>Hasna Recordings</string>
15
15
  <key>CFBundleExecutable</key>
16
16
  <string>Recordings</string>
17
17
  <key>CFBundleIdentifier</key>
18
18
  <string>com.hasna.recordings</string>
19
19
  <key>CFBundleVersion</key>
20
- <string>0.3.2</string>
20
+ <string>0.3.9</string>
21
21
  <key>CFBundlePackageType</key>
22
22
  <string>APPL</string>
23
23
  <key>CFBundleShortVersionString</key>
24
- <string>0.3.2</string>
24
+ <string>0.3.9</string>
25
25
  <key>LSMinimumSystemVersion</key>
26
26
  <string>26.0</string>
27
27
  <key>NSMicrophoneUsageDescription</key>
@@ -8,7 +8,7 @@ struct CLIRunnerTests {
8
8
  func bundledCLIIsPreferred() throws {
9
9
  let root = FileManager.default.temporaryDirectory
10
10
  .appendingPathComponent("recordings-bundled-cli-\(UUID().uuidString)")
11
- let app = root.appendingPathComponent("Recordings.app")
11
+ let app = root.appendingPathComponent("HasnaRecordings.app")
12
12
  let helper = app.appendingPathComponent("Contents/Helpers/recordings")
13
13
  let home = root.appendingPathComponent("home")
14
14
  let external = home.appendingPathComponent(".bun/bin/recordings")
@@ -34,7 +34,7 @@ struct CLIRunnerTests {
34
34
  func packagedAppDoesNotUseGlobalFallback() throws {
35
35
  let root = FileManager.default.temporaryDirectory
36
36
  .appendingPathComponent("recordings-missing-companion-\(UUID().uuidString)")
37
- let app = root.appendingPathComponent("Recordings.app")
37
+ let app = root.appendingPathComponent("HasnaRecordings.app")
38
38
  let home = root.appendingPathComponent("home")
39
39
  let external = home.appendingPathComponent(".bun/bin/recordings")
40
40
  try FileManager.default.createDirectory(at: app, withIntermediateDirectories: true)
@@ -6,7 +6,7 @@ struct RecordingStartGateTests {
6
6
  @Test("permission-only launch skips global handlers and terminates after handling")
7
7
  func permissionHelperLaunchPlan() {
8
8
  let plan = PermissionRequestLaunchPlan(arguments: [
9
- "/Applications/Recordings.app/Contents/MacOS/Recordings",
9
+ "/Applications/HasnaRecordings.app/Contents/MacOS/Recordings",
10
10
  "--request-permissions",
11
11
  "--open-permission-settings",
12
12
  ])
@@ -51,7 +51,7 @@ enum RecordingsBootstrapPreflight {
51
51
  let manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestData)
52
52
  try validateManifest(manifest, payload: payload)
53
53
 
54
- let app = arguments.payloadRoot + "/Applications/Recordings.app"
54
+ let app = arguments.payloadRoot + "/Applications/HasnaRecordings.app"
55
55
  let client = app + "/" + RecordingsUpdateConstants.updateClientRelativePath
56
56
  let broker = arguments.payloadRoot + "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
57
57
  let artifactVerifier = arguments.payloadRoot
@@ -7,7 +7,7 @@ import RecordingsUpdateProtocol
7
7
  /// operation; first installs use exclusive rename semantics and can never replace a
8
8
  /// leaf created concurrently in `/Applications`.
9
9
  final class ApplicationNamespace {
10
- private static let liveLeaf = "Recordings.app"
10
+ private static let liveLeaf = "HasnaRecordings.app"
11
11
  private static let previousLeaf = "previous.app"
12
12
  private static let failedCandidateLeaf = "failed-candidate.app"
13
13
 
@@ -22,7 +22,7 @@ final class ApplicationNamespace {
22
22
  .deletingLastPathComponent
23
23
  guard journal.applicationPath == RecordingsUpdateConstants.applicationPath,
24
24
  journal.candidateApplicationPath == expectedTransactionDirectory
25
- + "/candidate/Recordings.app",
25
+ + "/candidate/HasnaRecordings.app",
26
26
  journal.previousApplicationPath == nil ||
27
27
  journal.previousApplicationPath == expectedTransactionDirectory + "/previous.app"
28
28
  else {
@@ -276,7 +276,7 @@ final class UpdateBrokerSession: NSObject, RecordingsUpdateXPCProtocol {
276
276
  guard mkdir(candidateRoot, 0o700) == 0 else {
277
277
  throw BrokerOperationError.candidateRejected
278
278
  }
279
- let candidateApplication = candidateRoot + "/Recordings.app"
279
+ let candidateApplication = candidateRoot + "/HasnaRecordings.app"
280
280
  let verifierIDs = try verifierAccountIDs()
281
281
  try CanonicalTreeCopier.copyApplication(
282
282
  from: verifierOutput,
@@ -22,7 +22,7 @@ enum CanonicalTreeCopier {
22
22
  }
23
23
  defer { Darwin.close(source) }
24
24
  let destinationRoot = URL(fileURLWithPath: rootCandidatePath).deletingLastPathComponent().path
25
- guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "Recordings.app" else {
25
+ guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "HasnaRecordings.app" else {
26
26
  throw CanonicalCopyError.nonCanonicalRoot
27
27
  }
28
28
  let destination = Darwin.open(
@@ -253,7 +253,7 @@ struct DurableInstallJournal {
253
253
  let leaf = URL(fileURLWithPath: transactionDirectory).lastPathComponent
254
254
  guard leaf.hasPrefix("transaction-") else { throw InstallJournalError.invalidJournal }
255
255
  let transactionID = String(leaf.dropFirst("transaction-".count))
256
- let expectedCandidate = transactionDirectory + "/candidate/Recordings.app"
256
+ let expectedCandidate = transactionDirectory + "/candidate/HasnaRecordings.app"
257
257
  let activationPhases: Set<String> = [
258
258
  "prepared", "launch-barrier-pending", "launch-barrier-held",
259
259
  "swap-pending", "swapped", "previous-retaining",
@@ -201,8 +201,8 @@ struct ActivationRecoveryPolicyTests {
201
201
  "Contents/Helpers/recordings": 493,
202
202
  "Contents/Helpers/recordings-update-client": 493
203
203
  },
204
- "candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/Recordings.app",
205
- "application_path": "/Applications/Recordings.app"
204
+ "candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/HasnaRecordings.app",
205
+ "application_path": "/Applications/HasnaRecordings.app"
206
206
  }
207
207
  """#.utf8)
208
208
  let journal = try JSONDecoder().decode(BrokerInstallJournal.self, from: legacyJSON)
@@ -7,7 +7,7 @@ public enum RecordingsUpdateConstants {
7
7
  public static let rootMaintenanceSupported = false
8
8
  public static let keyRotationSupported = false
9
9
  public static let machServiceName = "com.hasna.recordings.updater"
10
- public static let applicationPath = "/Applications/Recordings.app"
10
+ public static let applicationPath = "/Applications/HasnaRecordings.app"
11
11
  public static let brokerExecutablePath = "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
12
12
  public static let stateRoot = "/Library/Application Support/Hasna/Recordings/Updates"
13
13
  public static let trustRoot = "/Library/Application Support/Hasna/Recordings/Trust"
@@ -729,7 +729,7 @@ int recordings_copy_canonical_application_tree(
729
729
  if (entry == NULL) break;
730
730
  if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
731
731
  visible_entries += 1;
732
- if (strcmp(entry->d_name, "Recordings.app") != 0) only_application = false;
732
+ if (strcmp(entry->d_name, "HasnaRecordings.app") != 0) only_application = false;
733
733
  }
734
734
  int status = errno;
735
735
  closedir(directory);
@@ -739,7 +739,7 @@ int recordings_copy_canonical_application_tree(
739
739
  status = copy_directory_at(
740
740
  verifier_output_directory_descriptor,
741
741
  root_candidate_directory_descriptor,
742
- "Recordings.app",
742
+ "HasnaRecordings.app",
743
743
  verifier_user_id,
744
744
  &budget
745
745
  );
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # Build, sign, notarize, and finalize a Recordings.app artifact.
2
+ # Build, sign, notarize, and finalize a HasnaRecordings.app artifact.
3
3
  # Usage: ./build.sh [debug|local] | ./build.sh release <initial-bootstrap|app-update>
4
4
 
5
5
  set -euo pipefail
@@ -119,16 +119,12 @@ if [ "$VARIANT" = "bar" ]; then
119
119
  VARIANT_SWIFT_FLAGS=(-Xswiftc -DRECORDINGS_BAR_ONLY)
120
120
  fi
121
121
  # Fleet naming rule (knowledge k_msxd5rz3_jfvl3i): every Hasna macOS app is
122
- # Hasna<Name>.app with 'Hasna' at the beginning. The full app keeps its historical
123
- # Recordings.app name (its rename is the rule's recorded follow-up decision); the
124
- # bar-only artifact is named per the rule NOW — HasnaRecordings.app. The 'bar' is the
125
- # variant, never a separate app name: bundle identifier stays com.hasna.recordings so
126
- # TCC keeps keying on bundle id + signing identity, and the executable inside the
127
- # bundle stays "Recordings".
128
- APP_BUNDLE_NAME="Recordings.app"
129
- if [ "$VARIANT" = "bar" ]; then
130
- APP_BUNDLE_NAME="HasnaRecordings.app"
131
- fi
122
+ # Hasna<Name>.app with 'Hasna' at the beginning. The bundle is named
123
+ # HasnaRecordings.app for BOTH variants (full and bar); the 'bar' is a variant, never
124
+ # a separate app name. Bundle identifier stays com.hasna.recordings so TCC keeps
125
+ # keying on bundle id + signing identity, and the executable inside the bundle stays
126
+ # "Recordings".
127
+ APP_BUNDLE_NAME="HasnaRecordings.app"
132
128
  readonly APP_BUNDLE_NAME
133
129
  APP_BASENAME="${APP_BUNDLE_NAME%.app}"
134
130
  PLIST_BUDDY="$(select_executable "/usr/libexec/PlistBuddy" "${RECORDINGS_TEST_PLIST_BUDDY_EXECUTABLE:-${PLIST_BUDDY:-}}")"
@@ -1260,7 +1256,7 @@ fi
1260
1256
 
1261
1257
  generate_and_verify_native_fs_guard
1262
1258
 
1263
- echo "Building Recordings.app ($MODE)..."
1259
+ echo "Building HasnaRecordings.app ($MODE)..."
1264
1260
  if [ "$MODE" = "release" ]; then
1265
1261
  OUTPUT_BUILD_DIR="$BUILD_ROOT/release-output"
1266
1262
  else
@@ -1426,15 +1422,15 @@ if [ "$MODE" = "release" ] && [ "$HOST_PLATFORM" = "Darwin" ]; then
1426
1422
  fi
1427
1423
  fi
1428
1424
  "$CP_EXECUTABLE" "$SOURCE_NATIVE_DIR/RecordingsLib/Info.plist" "$CONTENTS/Info.plist"
1425
+ # Bundle display name per the fleet naming rule (knowledge k_msxd5rz3_jfvl3i):
1426
+ # "Hasna Recordings" with 'Hasna' at the beginning, for both variants.
1427
+ "$PLIST_BUDDY" -c 'Set :CFBundleDisplayName Hasna Recordings' "$CONTENTS/Info.plist"
1429
1428
  if [ "$VARIANT" = "bar" ]; then
1430
1429
  # The bar is an accessory app: LSUIElement keeps it out of the Dock and the app
1431
1430
  # switcher, and the app's window-declaring control flow (RECORDINGS_BAR_ONLY)
1432
1431
  # keeps the workspace window absent. The bundle identifier stays com.hasna.recordings
1433
- # so TCC grants continue to key on bundle id + signing identity, and the bundle is
1434
- # named per the fleet rule (knowledge k_msxd5rz3_jfvl3i) — HasnaRecordings.app with
1435
- # the display name "Hasna Recordings"; 'Hasna' at the beginning of the name.
1432
+ # so TCC grants continue to key on bundle id + signing identity.
1436
1433
  "$PLIST_BUDDY" -c 'Add :LSUIElement bool true' "$CONTENTS/Info.plist"
1437
- "$PLIST_BUDDY" -c 'Set :CFBundleDisplayName Hasna Recordings' "$CONTENTS/Info.plist"
1438
1434
  fi
1439
1435
  VERSION="$("$PLIST_BUDDY" -c 'Print :CFBundleShortVersionString' "$CONTENTS/Info.plist")"
1440
1436
 
@@ -1670,8 +1666,6 @@ run_signed_helper_contract() {
1670
1666
  HOME="$contract_home"
1671
1667
  PATH="/usr/bin:/bin:/usr/sbin:/sbin"
1672
1668
  TMPDIR="$contract_home"
1673
- HASNA_RECORDINGS_CLIENT_STORE="sqlite"
1674
- RECORDINGS_CLIENT_STORE="sqlite"
1675
1669
  HASNA_RECORDINGS_DB_PATH="$contract_home/recordings.db"
1676
1670
  RECORDINGS_AUDIO_DIR="$contract_home/audio"
1677
1671
  )
@@ -1998,7 +1992,7 @@ if [ "$MODE" = "local" ]; then
1998
1992
  fi
1999
1993
 
2000
1994
  verify_signed_code "$HELPERS/recordings" "Companion CLI"
2001
- verify_signed_code "$APP_DIR" "Recordings.app"
1995
+ verify_signed_code "$APP_DIR" "HasnaRecordings.app"
2002
1996
  ARTIFACT_BASENAME="${APP_BASENAME}-${VERSION}-macos-${RELEASE_SUBTYPE}"
2003
1997
  NOTARY_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}-notarization.zip"
2004
1998
  FINAL_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}.zip"
@@ -2050,7 +2044,7 @@ run_xcrun stapler validate "$APP_DIR"
2050
2044
  run_release_sensitive_tool "$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$APP_DIR"
2051
2045
  run_release_sensitive_tool "$SYSPOLICY_CHECK_EXECUTABLE" distribution "$APP_DIR"
2052
2046
  verify_signed_code "$HELPERS/recordings" "Companion CLI"
2053
- verify_signed_code "$APP_DIR" "Recordings.app"
2047
+ verify_signed_code "$APP_DIR" "HasnaRecordings.app"
2054
2048
  run_codesign --verify --deep --strict --verbose=2 "$APP_DIR"
2055
2049
 
2056
2050
  run_release_sensitive_tool "$DITTO_EXECUTABLE" -c -k --sequesterRsrc --keepParent "$APP_DIR" "$FINAL_ARCHIVE"