@hasna/recordings 0.2.13 → 0.3.1
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.
- package/Dockerfile.package +4 -5
- package/README.md +113 -8
- package/bun.lock +2 -2
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
- package/dist/__tests__/preload.d.ts +1 -4
- package/dist/__tests__/preload.d.ts.map +1 -1
- package/dist/cli/desktop-snapshot.d.ts +20 -0
- package/dist/cli/desktop-snapshot.d.ts.map +1 -0
- package/dist/cli/index.js +2078 -277
- package/dist/cli/macos-permissions.d.ts +72 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -1
- package/dist/cli/macos-shortcut.d.ts +139 -0
- package/dist/cli/macos-shortcut.d.ts.map +1 -0
- package/dist/cli/trigger-probe.d.ts +172 -0
- package/dist/cli/trigger-probe.d.ts.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/http/client.d.ts +10 -7
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +634 -49
- package/dist/lib/capture-probe.d.ts +216 -0
- package/dist/lib/capture-probe.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +50 -0
- package/dist/lib/macos-bundle.d.ts.map +1 -0
- package/dist/lib/persistence-probe.d.ts +208 -0
- package/dist/lib/persistence-probe.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/retired-deployment-modes.d.ts +24 -0
- package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts +60 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +140 -56
- package/dist/server/cloud-config.d.ts +25 -3
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -7
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +187 -79
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +108 -49
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +25 -6
- package/scripts/build_native_fs_guard.sh +15 -2
- package/scripts/ci-linux-suite.ts +403 -0
- package/scripts/ci-native-build.ts +402 -0
- package/scripts/enforce_identity_migration.sh +110 -0
- package/scripts/generate-sdk.ts +1 -1
- package/scripts/install_macos_app.sh +138 -24
- package/scripts/macos_artifact.ts +205 -33
- package/scripts/native/realtime-settle-probe/README.md +39 -0
- package/scripts/native/realtime-settle-probe/main.swift +135 -0
- package/scripts/policy/local-only-approved-targets.txt +21 -0
- package/scripts/read_local_only_targets.sh +203 -0
- package/scripts/scan-artifact.ts +40 -0
- package/scripts/set-version.ts +145 -0
- package/scripts/vacuity-manifest-gen.ts +96 -0
- package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
- package/scripts/vacuity-manifests/install-chain.tsv +13 -0
- package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
- package/scripts/vacuity-manifests/reorder.tsv +4 -0
- package/scripts/vacuity-manifests/source-side.tsv +15 -0
- package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
- package/scripts/vacuity-manifests/version-sites.tsv +61 -0
- package/scripts/vacuity-mutation-battery.ts +266 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +14 -7
- package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
- package/src/native/Recordings/App/SidebarView.swift +1 -1
- package/src/native/Recordings/Package.swift +24 -5
- package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
- package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
- package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
- package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
- package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
- package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
- package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
- package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
- package/src/native/Recordings/build.sh +75 -9
- 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
|
+
}
|
package/scripts/generate-sdk.ts
CHANGED
|
@@ -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
|
|
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));";
|