@hasna/recordings 0.2.14 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/Dockerfile.package +4 -5
  2. package/README.md +113 -8
  3. package/bun.lock +2 -2
  4. package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
  5. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
  6. package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
  7. package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
  8. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
  9. package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
  10. package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
  11. package/dist/__tests__/preload.d.ts +1 -4
  12. package/dist/__tests__/preload.d.ts.map +1 -1
  13. package/dist/cli/desktop-snapshot.d.ts +20 -0
  14. package/dist/cli/desktop-snapshot.d.ts.map +1 -0
  15. package/dist/cli/index.js +2094 -280
  16. package/dist/cli/macos-permissions.d.ts +72 -0
  17. package/dist/cli/macos-permissions.d.ts.map +1 -1
  18. package/dist/cli/macos-shortcut.d.ts +139 -0
  19. package/dist/cli/macos-shortcut.d.ts.map +1 -0
  20. package/dist/cli/trigger-probe.d.ts +172 -0
  21. package/dist/cli/trigger-probe.d.ts.map +1 -0
  22. package/dist/db/database.d.ts +9 -0
  23. package/dist/db/database.d.ts.map +1 -1
  24. package/dist/http/client.d.ts +19 -7
  25. package/dist/http/client.d.ts.map +1 -1
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +635 -50
  29. package/dist/lib/capture-probe.d.ts +216 -0
  30. package/dist/lib/capture-probe.d.ts.map +1 -0
  31. package/dist/lib/config.d.ts.map +1 -1
  32. package/dist/lib/macos-bundle.d.ts +50 -0
  33. package/dist/lib/macos-bundle.d.ts.map +1 -0
  34. package/dist/lib/persistence-probe.d.ts +208 -0
  35. package/dist/lib/persistence-probe.d.ts.map +1 -0
  36. package/dist/lib/recorder.d.ts.map +1 -1
  37. package/dist/lib/retired-deployment-modes.d.ts +24 -0
  38. package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
  39. package/dist/lib/transcriber.d.ts +60 -0
  40. package/dist/lib/transcriber.d.ts.map +1 -1
  41. package/dist/mcp/index.js +141 -57
  42. package/dist/server/cloud-config.d.ts +25 -3
  43. package/dist/server/cloud-config.d.ts.map +1 -1
  44. package/dist/server/cloud-readiness.d.ts.map +1 -1
  45. package/dist/server/cloud.d.ts +8 -7
  46. package/dist/server/cloud.d.ts.map +1 -1
  47. package/dist/server/index.js +188 -80
  48. package/dist/server/migrate-command.d.ts.map +1 -1
  49. package/dist/server/serve.d.ts.map +1 -1
  50. package/dist/storage.d.ts +2 -2
  51. package/dist/storage.d.ts.map +1 -1
  52. package/dist/storage.js +109 -50
  53. package/dist/store.d.ts +1 -1
  54. package/dist/store.d.ts.map +1 -1
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.d.ts.map +1 -1
  57. package/package.json +25 -6
  58. package/scripts/ci-linux-suite.ts +403 -0
  59. package/scripts/ci-native-build.ts +402 -0
  60. package/scripts/enforce_identity_migration.sh +110 -0
  61. package/scripts/generate-sdk.ts +1 -1
  62. package/scripts/install_macos_app.sh +192 -31
  63. package/scripts/macos_artifact.ts +323 -43
  64. package/scripts/native/realtime-settle-probe/README.md +39 -0
  65. package/scripts/native/realtime-settle-probe/main.swift +135 -0
  66. package/scripts/policy/local-only-approved-targets.txt +21 -0
  67. package/scripts/read_local_only_targets.sh +203 -0
  68. package/scripts/scan-artifact.ts +40 -0
  69. package/scripts/set-version.ts +145 -0
  70. package/scripts/smoke_macos_app.sh +62 -16
  71. package/scripts/vacuity-manifest-gen.ts +96 -0
  72. package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
  73. package/scripts/vacuity-manifests/install-chain.tsv +13 -0
  74. package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
  75. package/scripts/vacuity-manifests/reorder.tsv +4 -0
  76. package/scripts/vacuity-manifests/source-side.tsv +15 -0
  77. package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
  78. package/scripts/vacuity-manifests/version-sites.tsv +61 -0
  79. package/scripts/vacuity-mutation-battery.ts +266 -0
  80. package/src/native/Recordings/App/MenuBarStatusView.swift +23 -11
  81. package/src/native/Recordings/App/RecordingsApp.swift +37 -1
  82. package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
  83. package/src/native/Recordings/App/SidebarView.swift +1 -1
  84. package/src/native/Recordings/Package.swift +24 -5
  85. package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
  86. package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
  87. package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
  88. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
  89. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
  90. package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
  91. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
  92. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
  93. package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
  94. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
  95. package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
  96. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
  97. package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
  98. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
  99. package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
  100. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
  101. package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
  102. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
  103. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
  104. package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
  105. package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
  106. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
  107. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
  108. package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
  109. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
  110. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
  111. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
  112. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
  113. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
  114. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
  115. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
  116. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
  117. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
  118. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
  119. package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
  120. package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
  121. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
  122. package/src/native/Recordings/build.sh +226 -20
  123. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Compile the native half and compare the compiler's error SET against a recorded baseline.
4
+ *
5
+ * Why any of this exists: `main` does not compile. The first CI run this repository ever had
6
+ * discovered it, and the two obvious responses are both wrong.
7
+ *
8
+ * Gate on a green build — the workflow is red on arrival for a defect it does not own, so it gets
9
+ * ignored or bypassed, and this repository ends up back where it started.
10
+ * Let the build fail softly (`continue-on-error`) — the native half then has NO regression
11
+ * protection whatsoever, and a check that renders green while 87 Swift files went unverified is
12
+ * the precise false assurance the workflow exists to end.
13
+ *
14
+ * So the recorded errors are baselined and everything they do NOT block is gated properly:
15
+ *
16
+ * 1. The whole package is built with --build-tests. A NEW error fails the job; a failure with no
17
+ * diagnostic attributable to this repository also fails, since it cannot be compared to the
18
+ * baseline at all; a success while the baseline is non-empty fails as stale.
19
+ * 2. Because that build stops scheduling after its first failure — measured: 41 of 140 tasks, all
20
+ * of them third-party — step 1 alone proves nothing about the rest of the package. So every
21
+ * target not transitively blocked by a baselined error is then built INDIVIDUALLY and required
22
+ * to be clean, and required to show compile activity, because an exit status on its own does not
23
+ * distinguish "compiled clean" from "did nothing".
24
+ * 3. Blocked targets are built too, and any that now builds cleanly fails the job as a stale
25
+ * baseline entry. That is what stops the baseline being permanent by default.
26
+ *
27
+ * Usage: bun scripts/ci-native-build.ts [--package-path <dir>]
28
+ */
29
+ import { readFileSync } from "node:fs";
30
+ import { appendFileSync } from "node:fs";
31
+ import { relative, resolve } from "node:path";
32
+
33
+ export const KNOWN_ERRORS_FILE = ".github/native-known-errors.txt";
34
+
35
+ /** Parse the baseline file: one `path: message` signature per line, `#` comments dropped. */
36
+ export function parseKnownErrors(text: string): string[] {
37
+ return text
38
+ .split("\n")
39
+ .map((line) => line.trim())
40
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
41
+ }
42
+
43
+ /**
44
+ * Normalize one clang/swift diagnostic message into a stable signature body.
45
+ *
46
+ * Truncated at the first `;` and stripped of the trailing `[-Wflag]`, because clang appends both the
47
+ * standards rationale and the controlling flag to the same message, and neither is a property of the
48
+ * defect. Keeping them would make the signature churn on a compiler upgrade.
49
+ */
50
+ export function normalizeMessage(message: string): string {
51
+ return message
52
+ .replace(/\s*\[-[A-Za-z0-9=_-]+\]\s*$/, "")
53
+ .split(";")[0]!
54
+ .trim();
55
+ }
56
+
57
+ /**
58
+ * Extract error signatures from combined build output.
59
+ *
60
+ * Deduplicated: a header included by several targets reports the same error once per target, and a
61
+ * baseline that had to list each repetition would break whenever a target was added.
62
+ */
63
+ export function errorSignatures(output: string, repoRoot: string): string[] {
64
+ const signatures = new Set<string>();
65
+ // Anchored on the whole line so that a message merely QUOTING the word "error:" — for instance a
66
+ // Swift string literal echoed back in a note — cannot be read as a diagnostic of its own.
67
+ //
68
+ // `[^:\n]` rather than `[^:]` for the path tail, and the newline exclusion is not cosmetic: with
69
+ // `[^:]` the class happily spans line breaks, so the path captured from real build output began at
70
+ // an unrelated progress line several lines earlier and produced a signature with a newline inside
71
+ // it. The contract test for this function caught exactly that.
72
+ const pattern = /^([^\s:][^:\n]*):(\d+):(\d+): error: (.+)$/gm;
73
+ for (const match of output.matchAll(pattern)) {
74
+ const [, rawPath, , , message] = match;
75
+ if (!rawPath || !message) continue;
76
+ const absolute = resolve(repoRoot, rawPath);
77
+ const path = relative(repoRoot, absolute).split("\\").join("/");
78
+ // A diagnostic from inside the SDK or the build cache is not this repository's source, and
79
+ // pinning one would make the baseline a function of the runner image. `.build` is matched as a
80
+ // path SEGMENT anywhere, not as a prefix: SwiftPM's cache sits at src/native/Recordings/.build,
81
+ // so a prefix test never fires.
82
+ if (path.startsWith("..") || path.split("/").includes(".build")) continue;
83
+ signatures.add(`${path}: ${normalizeMessage(message)}`);
84
+ }
85
+ return [...signatures].sort();
86
+ }
87
+
88
+ export type Verdict =
89
+ | { kind: "clean" }
90
+ | { kind: "stale-baseline"; reason: string }
91
+ | { kind: "unattributable-failure" }
92
+ | { kind: "quarantined"; observed: string[] }
93
+ | { kind: "regression"; introduced: string[] };
94
+
95
+ /**
96
+ * Decide the outcome from the build's exit status and its error set.
97
+ *
98
+ * `disappeared` is only reported when nothing new appeared. Compilation stops early, so a brand new
99
+ * error in an earlier translation unit can prevent a recorded one from ever being emitted; blaming
100
+ * the baseline in that case would bury the actual regression under a bookkeeping complaint.
101
+ */
102
+ export function verdict(succeeded: boolean, observed: string[], known: string[]): Verdict {
103
+ const introduced = observed.filter((signature) => !known.includes(signature));
104
+ if (introduced.length > 0) return { kind: "regression", introduced };
105
+ // A failed build from which NO diagnostic could be attributed to this repository cannot be compared
106
+ // against the baseline at all, so it must fail rather than compare equal to it.
107
+ //
108
+ // This is not hypothetical. `errorSignatures` only recognises `path:line:col: error:`, so a linker
109
+ // failure ("ld: symbol(s) not found", "clang: error: linker command failed"), a swift-driver
110
+ // "error: emit-module command failed", or an out-of-disk abort all yield zero signatures. Without
111
+ // this branch, `verdict(false, [], [])` returned `quarantined` and the job exited 0 — a build that
112
+ // failed for a reason nobody parsed would have been GREEN, and green FOREVER once the baseline was
113
+ // emptied, which is exactly what this file's own remediation text tells you to do.
114
+ if (!succeeded && observed.length === 0) return { kind: "unattributable-failure" };
115
+ if (succeeded) {
116
+ if (known.length === 0) return { kind: "clean" };
117
+ return {
118
+ kind: "stale-baseline",
119
+ reason:
120
+ `The native build now SUCCEEDS while ${KNOWN_ERRORS_FILE} still records ${known.length} ` +
121
+ "error(s). Delete the file's entries so this becomes a plain build gate.",
122
+ };
123
+ }
124
+ // Deliberately NOT reporting recorded errors that failed to appear. The whole-package build stops
125
+ // scheduling after its first failure, so most recorded errors are never REACHED and their absence
126
+ // says nothing about whether they are fixed. An earlier version treated that as a stale baseline
127
+ // and failed the job for bookkeeping every single run. Staleness is detected per target instead,
128
+ // where a target that builds clean is real evidence its entries are gone.
129
+ return { kind: "quarantined", observed };
130
+ }
131
+
132
+ export type TargetInfo = { name: string; dir: string; dependencies: string[] };
133
+
134
+ /**
135
+ * Read the package's targets and their internal dependencies from `swift package dump-package`.
136
+ *
137
+ * The manifest is evaluated rather than parsed, and dumping it works even when the package does not
138
+ * COMPILE, so this stays available in exactly the situation it is needed. A regex over Package.swift
139
+ * would have to re-implement target/path/dependency resolution and would drift the first time a
140
+ * target was declared in a way the regex did not anticipate.
141
+ */
142
+ export function targetsFromDump(dump: unknown, packagePath: string): TargetInfo[] {
143
+ const raw = (dump as { targets?: unknown[] })?.targets;
144
+ if (!Array.isArray(raw)) throw new Error("dump-package produced no targets array");
145
+ return raw.map((entry) => {
146
+ const target = entry as {
147
+ name: string;
148
+ path?: string | null;
149
+ dependencies?: unknown[];
150
+ };
151
+ const dependencies: string[] = [];
152
+ for (const dependency of target.dependencies ?? []) {
153
+ // `byName` and `target` are internal references; `product` names a package dependency, which
154
+ // cannot be the thing blocked by an error in THIS repository's sources.
155
+ const named = dependency as { byName?: unknown[]; target?: unknown[] };
156
+ const reference = named.byName?.[0] ?? named.target?.[0];
157
+ if (typeof reference === "string") dependencies.push(reference);
158
+ }
159
+ return {
160
+ name: target.name,
161
+ // SwiftPM defaults an omitted path to Sources/<name>; every target in this package declares
162
+ // one, but relying on that would make the fallback silently wrong rather than absent.
163
+ dir: `${packagePath}/${target.path ?? `Sources/${target.name}`}`.replace(/\/+$/, ""),
164
+ dependencies,
165
+ };
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Targets that cannot be built because a recorded error lives in them, plus everything that depends
171
+ * on those transitively.
172
+ *
173
+ * This is what makes the per-target gate honest. `swift build --build-tests` stops scheduling work
174
+ * after the first failure, and measured on run 30303366777 it reached 41 of 140 tasks and compiled
175
+ * only third-party dependencies — not one of this package's own Swift targets. So "the build failed
176
+ * with only known errors" proves nothing about the other targets: the compiler never looked at them.
177
+ * Everything not blocked is therefore built individually and required to be clean.
178
+ */
179
+ export function blockedTargets(targets: TargetInfo[], errorFiles: string[]): string[] {
180
+ const blocked = new Set<string>();
181
+ for (const target of targets) {
182
+ if (errorFiles.some((file) => file === target.dir || file.startsWith(`${target.dir}/`))) {
183
+ blocked.add(target.name);
184
+ }
185
+ }
186
+ // Transitive closure. Iterate to a fixed point rather than recursing, so a dependency cycle in a
187
+ // malformed manifest cannot turn this into a stack overflow.
188
+ for (let changed = true; changed; ) {
189
+ changed = false;
190
+ for (const target of targets) {
191
+ if (blocked.has(target.name)) continue;
192
+ if (target.dependencies.some((dependency) => blocked.has(dependency))) {
193
+ blocked.add(target.name);
194
+ changed = true;
195
+ }
196
+ }
197
+ }
198
+ return [...blocked].sort();
199
+ }
200
+
201
+ /** The source file part of a `<path>: <message>` signature. */
202
+ export function signatureFile(signature: string): string {
203
+ return signature.split(": ")[0]!;
204
+ }
205
+
206
+ /**
207
+ * Target names SwiftPM reported doing compile work for.
208
+ *
209
+ * This turns "the build exited 0" into evidence that something was actually compiled. Without it a
210
+ * per-target build that did nothing at all is indistinguishable from one that compiled the target
211
+ * clean — and "did nothing" is the normal outcome when the target is already up to date, so the
212
+ * distinction is not hypothetical. Found by reading run 30305889651's log and discovering it contains
213
+ * no `Compiling RecordingsLib` line anywhere, because successful output was being discarded: three
214
+ * targets were reported `ok` on no recorded evidence whatsoever.
215
+ *
216
+ * Accumulated across every phase of the job rather than per invocation, because building one target
217
+ * warms its dependencies, so a dependency's own later invocation legitimately prints nothing.
218
+ */
219
+ export function compiledTargets(output: string): string[] {
220
+ const seen = new Set<string>();
221
+ for (const match of output.matchAll(/^(?:\[\d+\/\d+\] )?(?:Compiling|Emitting module) (\w+)\b/gm)) {
222
+ if (match[1]) seen.add(match[1]);
223
+ }
224
+ return [...seen].sort();
225
+ }
226
+
227
+ /** Emit a GitHub Actions step output when running under Actions; a no-op elsewhere. */
228
+ function setOutput(name: string, value: string): void {
229
+ const file = process.env.GITHUB_OUTPUT;
230
+ if (file) appendFileSync(file, `${name}=${value}\n`);
231
+ }
232
+
233
+ /** Run a command, streaming nothing, returning combined output and exit status. */
234
+ async function run(command: string[]): Promise<{ output: string; exitCode: number }> {
235
+ const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
236
+ const [stdout, stderr, exitCode] = await Promise.all([
237
+ new Response(proc.stdout).text(),
238
+ new Response(proc.stderr).text(),
239
+ proc.exited,
240
+ ]);
241
+ return { output: `${stdout}\n${stderr}`, exitCode };
242
+ }
243
+
244
+ async function main(argv: string[]): Promise<void> {
245
+ const repoRoot = process.cwd();
246
+ const pathIndex = argv.indexOf("--package-path");
247
+ const packagePath = pathIndex >= 0 ? argv[pathIndex + 1]! : "src/native/Recordings";
248
+
249
+ // --build-tests compiles the test targets as well as the products, so one invocation covers every
250
+ // line of native code the package knows about — WHEN it succeeds. When it fails it stops early,
251
+ // which is why the per-target pass below exists.
252
+ const whole = ["swift", "build", "--build-tests", "--package-path", packagePath];
253
+ console.log(`$ ${whole.join(" ")}`);
254
+ const { output, exitCode } = await run(whole);
255
+ process.stdout.write(output);
256
+
257
+ const known = parseKnownErrors(readFileSync(`${repoRoot}/${KNOWN_ERRORS_FILE}`, "utf8"));
258
+ const observed = errorSignatures(output, repoRoot);
259
+ const result = verdict(exitCode === 0, observed, known);
260
+ setOutput("compiled", exitCode === 0 ? "true" : "false");
261
+
262
+ if (result.kind === "clean") {
263
+ console.log("\nNative build is clean: every target and every test target compiles, fully gated.");
264
+ return;
265
+ }
266
+ if (result.kind === "regression") {
267
+ console.log(
268
+ `\n::error title=New native compile error::${result.introduced.length} compile error(s) ` +
269
+ `not recorded in ${KNOWN_ERRORS_FILE} appeared in this change: ` +
270
+ result.introduced.join(" | "),
271
+ );
272
+ process.exit(1);
273
+ }
274
+ if (result.kind === "unattributable-failure") {
275
+ console.log(
276
+ "\n::error title=Native build failed for an unparsed reason::The build exited non-zero but no " +
277
+ "compile diagnostic could be attributed to a file in this repository, so the failure cannot " +
278
+ `be compared against ${KNOWN_ERRORS_FILE}. A linker error, a driver error or an aborted ` +
279
+ "build all look like this. Read the log above; do NOT record anything in the baseline for it.",
280
+ );
281
+ process.exit(1);
282
+ }
283
+ if (result.kind === "stale-baseline") {
284
+ console.log(`\n::error title=Stale native baseline::${result.reason}`);
285
+ process.exit(1);
286
+ }
287
+
288
+ // The whole-package build failed with exactly the recorded errors. That alone is NOT evidence about
289
+ // any other target, because the build aborted before reaching them: measured on run 30303366777 it
290
+ // got to 41 of 140 tasks and compiled only third-party dependencies. So build every target that is
291
+ // not blocked by a recorded error, individually, and require each to be clean. This is the
292
+ // difference between "no new error appeared in the 30% that compiled" and a real gate.
293
+ const dump = await run(["swift", "package", "dump-package", "--package-path", packagePath]);
294
+ if (dump.exitCode !== 0) {
295
+ console.log(`\n::error title=Cannot read the package manifest::${dump.output.trim().slice(0, 400)}`);
296
+ process.exit(1);
297
+ }
298
+ const targets = targetsFromDump(JSON.parse(dump.output), packagePath);
299
+ // Blocked is derived from the BASELINE, not from what this build happened to report. The build
300
+ // aborts early, so `observed` names only the errors in whatever compiled first — on this package
301
+ // that is the C launcher, and deriving from it left the four recorded Swift errors in
302
+ // Updater/Protocol unblocked, so the gate demanded that a known-broken target build cleanly.
303
+ const blocked = blockedTargets(targets, known.map(signatureFile));
304
+ const gated = targets.filter((target) => !blocked.includes(target.name)).map((t) => t.name);
305
+
306
+ console.log(
307
+ `\nWhole-package build stopped at the recorded errors. Building each target individually: ` +
308
+ `${gated.length} gated, ${blocked.length} blocked by the baseline.\n` +
309
+ ` blocked: ${blocked.join(", ") || "(none)"}\n`,
310
+ );
311
+ if (gated.length === 0) {
312
+ console.log(
313
+ "\n::error title=Nothing could be gated::Every target is blocked by a recorded error, so this " +
314
+ "job proves nothing at all. Fix the recorded errors rather than keeping the baseline.",
315
+ );
316
+ process.exit(1);
317
+ }
318
+
319
+ // Surfaced so the `scope` job can name what went uncompiled instead of saying "the native half"
320
+ // and leaving a reader to guess how much of it that is.
321
+ setOutput("blocked", blocked.join(", "));
322
+ setOutput("gatedTargets", String(gated.length));
323
+
324
+ // Compile activity from every phase, starting with the whole-package attempt.
325
+ const compiled = new Set<string>(compiledTargets(output));
326
+
327
+ const buildTarget = async (name: string) => {
328
+ const attempt = await run(["swift", "build", "--target", name, "--package-path", packagePath]);
329
+ for (const seen of compiledTargets(attempt.output)) compiled.add(seen);
330
+ return {
331
+ clean: attempt.exitCode === 0,
332
+ introduced: errorSignatures(attempt.output, repoRoot).filter((s) => !known.includes(s)),
333
+ output: attempt.output,
334
+ };
335
+ };
336
+
337
+ const failures: string[] = [];
338
+ for (const name of gated) {
339
+ const { clean, introduced, output: targetOutput } = await buildTarget(name);
340
+ if (clean && introduced.length === 0) {
341
+ // Print the compile lines rather than a bare "ok". An `ok` with no evidence behind it is the
342
+ // same species of claim this whole workflow exists to stop accepting.
343
+ const evidence = targetOutput
344
+ .split("\n")
345
+ .filter((line) => /(?:Compiling|Emitting module) /.test(line))
346
+ .length;
347
+ console.log(` ok ${name} (${evidence} compile step(s))`);
348
+ continue;
349
+ }
350
+ console.log(` FAILED ${name}`);
351
+ process.stdout.write(targetOutput);
352
+ failures.push(name);
353
+ }
354
+
355
+ // A gated target that never appeared in any compile line was not verified, whatever its exit
356
+ // status said. Reported as a hard failure: a gate that cannot show its work is not a gate.
357
+ const unproven = gated.filter((name) => !compiled.has(name));
358
+ if (unproven.length > 0) {
359
+ console.log(
360
+ `\n::error title=Target reported clean without compiling::${unproven.join(", ")} exited 0 but ` +
361
+ "SwiftPM never reported compiling or emitting a module for them, so nothing was verified. " +
362
+ `Targets observed compiling: ${[...compiled].sort().join(", ") || "(none)"}`,
363
+ );
364
+ process.exit(1);
365
+ }
366
+
367
+ // The baseline's expiry date, and the reason it is checked HERE. A blocked target that now builds
368
+ // clean is real evidence that its recorded errors are gone, unlike their mere absence from an
369
+ // early-aborting whole-package build. Without this the baseline is permanent by default.
370
+ const revived: string[] = [];
371
+ for (const name of blocked) {
372
+ const { clean } = await buildTarget(name);
373
+ console.log(` ${clean ? "NOW CLEAN" : "blocked "} ${name}`);
374
+ if (clean) revived.push(name);
375
+ }
376
+
377
+ if (failures.length > 0) {
378
+ console.log(
379
+ `\n::error title=Native target failed to compile::${failures.join(", ")}. Not blocked by any ` +
380
+ `error recorded in ${KNOWN_ERRORS_FILE}, so this is a break introduced by this change.`,
381
+ );
382
+ process.exit(1);
383
+ }
384
+ if (revived.length > 0) {
385
+ console.log(
386
+ `\n::error title=Stale native baseline::${revived.join(", ")} now build cleanly. Delete their ` +
387
+ `entries from ${KNOWN_ERRORS_FILE} so these targets become gated instead of exempt.`,
388
+ );
389
+ process.exit(1);
390
+ }
391
+
392
+ console.log(
393
+ `\n::warning title=Native half only PARTLY verified::${gated.length} target(s) compile cleanly ` +
394
+ `(${gated.join(", ")}), but ${blocked.length} could NOT be compiled at all ` +
395
+ `(${blocked.join(", ")}) because of ${known.length} pre-existing error(s) recorded in ` +
396
+ `${KNOWN_ERRORS_FILE}. Nothing in those blocked targets is verified by this run.`,
397
+ );
398
+ }
399
+
400
+ if (import.meta.main) {
401
+ await main(process.argv.slice(2));
402
+ }
@@ -0,0 +1,110 @@
1
+ # shellcheck shell=bash
2
+ # Shared enforcement for the designated-requirement (code identity) migration gate,
3
+ # sourced by scripts/install_macos_app.sh.
4
+ #
5
+ # Replacing an installed app whose designated requirement the candidate does not
6
+ # satisfy is what makes macOS treat the candidate as a different program: the
7
+ # Microphone and Accessibility grants recorded against the installed requirement stop
8
+ # applying. That is true for every artifact policy, not only for release. Local-only
9
+ # artifacts are ad-hoc signed (src/native/Recordings/build.sh sets CODESIGN_IDENTITY="-"),
10
+ # so their designated requirement is CDHash-based and a local-only install over a
11
+ # certificate-rooted bundle always destroys the grant. This gate therefore lives in one
12
+ # function that the installer calls unconditionally, rather than in policy-gated
13
+ # conditionals that a new policy can silently escape.
14
+ #
15
+ # It is deliberately a pure function of already-computed values with no side effects and
16
+ # no macOS-only tools, so the whole decision table is executable on any host — see
17
+ # src/__tests__/identity-migration-guard.test.ts. Every input is validated before it is
18
+ # interpreted: an empty, unset, or unrecognised value denies. There is no default-allow
19
+ # branch.
20
+ #
21
+ # The two approvals are distinct on purpose. --allow-signing-identity-migration approves a
22
+ # release signer rotation and is bound to exact operator-supplied old/new digests;
23
+ # --allow-adhoc-identity-migration approves an ad-hoc local-only replacement. Approving
24
+ # one is not approving the other, so neither flag is honoured for the other policy.
25
+
26
+ # recordings_enforce_identity_migration <artifact-policy> <identity-migration> \
27
+ # <allow-release-migration> <allow-adhoc-migration> <previous-identity-sha256> \
28
+ # <candidate-identity-sha256> <expected-old-identity-sha256> <expected-new-identity-sha256>
29
+ # Returns 0 only when replacing the installed app is explicitly permitted, and non-zero
30
+ # with a message on stderr in every other case, including malformed input.
31
+ recordings_enforce_identity_migration() {
32
+ if [ "$#" -ne 8 ]; then
33
+ echo "Identity-migration enforcement received ${#} arguments instead of 8." >&2
34
+ return 1
35
+ fi
36
+ local artifact_policy="$1"
37
+ local identity_migration="$2"
38
+ local allow_release_migration="$3"
39
+ local allow_adhoc_migration="$4"
40
+ local previous_identity_sha256="$5"
41
+ local candidate_identity_sha256="$6"
42
+ local expected_old_identity_sha256="$7"
43
+ local expected_new_identity_sha256="$8"
44
+
45
+ # `[ x -eq y ]` on a non-numeric value is a fatal syntax error under `set -e` in some
46
+ # shells and a silent 0 in others, so the three state bits are shape-checked here
47
+ # instead of being fed straight to an arithmetic test.
48
+ case "$identity_migration" in
49
+ 0 | 1) ;;
50
+ *)
51
+ echo "Identity-migration state ${identity_migration:-<empty>} is not a 0/1 decision; refusing to replace the installed app." >&2
52
+ return 1
53
+ ;;
54
+ esac
55
+ case "$allow_release_migration" in
56
+ 0 | 1) ;;
57
+ *)
58
+ echo "Release identity-migration approval ${allow_release_migration:-<empty>} is not a 0/1 decision." >&2
59
+ return 1
60
+ ;;
61
+ esac
62
+ case "$allow_adhoc_migration" in
63
+ 0 | 1) ;;
64
+ *)
65
+ echo "Ad-hoc identity-migration approval ${allow_adhoc_migration:-<empty>} is not a 0/1 decision." >&2
66
+ return 1
67
+ ;;
68
+ esac
69
+
70
+ # An unrecognised policy — including an empty one — has no approval flag and therefore
71
+ # cannot be approved. Denying here is what keeps a future policy from inheriting the
72
+ # release-only gating that made this gate unenforced for local_only.
73
+ local approval
74
+ case "$artifact_policy" in
75
+ release) approval="$allow_release_migration" ;;
76
+ local_only) approval="$allow_adhoc_migration" ;;
77
+ *)
78
+ echo "Identity-migration enforcement does not recognise the artifact policy ${artifact_policy:-<empty>}." >&2
79
+ return 1
80
+ ;;
81
+ esac
82
+
83
+ if [ "$identity_migration" -eq 1 ] && [ "$approval" -ne 1 ]; then
84
+ if [ "$artifact_policy" = "release" ]; then
85
+ echo "Candidate and existing app designated requirements are not mutually compatible; review the signer change and rerun once with --allow-signing-identity-migration." >&2
86
+ else
87
+ echo "Candidate and existing app designated requirements are not mutually compatible (installed ${previous_identity_sha256} -> candidate ${candidate_identity_sha256}); this local-only artifact is ad-hoc signed, so replacing the installed app voids its Microphone and Accessibility grants. Rerun once with --allow-adhoc-identity-migration to accept that." >&2
88
+ fi
89
+ return 1
90
+ fi
91
+ # An approval that is not needed means the operator and the installer disagree about
92
+ # what is being replaced, and one of them is wrong. It also stops automation from
93
+ # carrying the flag permanently, which would turn the gate back into a no-op.
94
+ if [ "$identity_migration" -eq 0 ] && [ "$approval" -eq 1 ]; then
95
+ echo "Identity migration approval was supplied but no identity migration is required." >&2
96
+ return 1
97
+ fi
98
+ # Release migrations are additionally pinned to the exact operator-approved pair. A
99
+ # local-only candidate is ad-hoc, so its digest is derived from a CDHash that changes
100
+ # with every build; pinning it would only restate the artifact in hand. The distinct
101
+ # approval flag carries that consent instead.
102
+ if [ "$artifact_policy" = "release" ] && [ "$identity_migration" -eq 1 ] && {
103
+ [ "$previous_identity_sha256" != "$expected_old_identity_sha256" ] ||
104
+ [ "$candidate_identity_sha256" != "$expected_new_identity_sha256" ];
105
+ }; then
106
+ echo "Signing identity migration does not match the exact operator-approved old/new identities." >&2
107
+ return 1
108
+ fi
109
+ return 0
110
+ }
@@ -20,7 +20,7 @@ const { code, operations, warnings } = generateSdkFromOpenApi(spec as never, {
20
20
  apiKeyHeader: "x-api-key",
21
21
  });
22
22
 
23
- // @hasna/contracts 0.4.2 stringifies query arrays. Preserve OpenAPI's
23
+ // @hasna/contracts 0.8.4 stringifies query arrays. Preserve OpenAPI's
24
24
  // form+explode semantics until the package generator ships array support.
25
25
  const scalarQuerySerialization =
26
26
  " if (value !== undefined && value !== null) url.searchParams.set(key, String(value));";