SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
19.2 KB · 375 lines swift
Raw Blame History
1//2//  VerifyHarness.swift3//  Zyquo Atlas4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Phase 7 verification. Exercises the *Atlas* stack against live APIs with real9//  keys: (1) content extraction on real pages, (2) a grounded summarize across10//  every catalog model through the ported provider clients, (3) the core AI11//  action matrix, and (4) cancel-on-navigation. Results → stdout + docs/12//  VERIFICATION.md. Keys come from the environment (source .env.keys) and are13//  never logged or persisted. `--load-vault` seeds the vault; `--quick` runs one14//  model per provider.15//1617import Foundation18import WebKit19import AppKit2021@MainActor22enum VerifyHarness {23    static let environmentKeys: [ProviderID: String] = [24        .openai: "OPENAI_API_KEY", .anthropic: "ANTHROPIC_API_KEY", .xai: "XAI_API_KEY",25        .mistral: "MISTRAL_API_KEY", .gemini: "GEMINI_API_KEY", .qwen: "DASHSCOPE_API_KEY",26        .deepseek: "DEEPSEEK_API_KEY", .kimi: "MOONSHOT_API_KEY", .perplexity: "PERPLEXITY_API_KEY",27        .together: "TOGETHER_API_KEY", .deepinfra: "DEEPINFRA_API_KEY", .cerebras: "CEREBRAS_API_KEY",28    ]2930    struct Result {31        let section: String32        let provider: String33        let subject: String34        let test: String35        let passed: Bool36        let latency: TimeInterval?37        let detail: String38    }3940    // MARK: - Entry4142    static func run(arguments: [String]) async -> Int32 {43        let quick = arguments.contains("--quick")44        let onlyProvider = value(after: "--provider", in: arguments).flatMap { ProviderID(rawValue: $0) }45        let catalog = ModelCatalog()4647        print("Zyquo Atlas — Phase 7 verification\(quick ? " (quick)" : "")\n")48        var results: [Result] = []4950        // 1) Extraction-quality suite (headless real pages).51        let (extraction, sample) = await verifyExtraction()52        results += extraction5354        // A grounded PageContext for the model sweep + action matrix. Trim so55        // every one of 170 model calls stays cheap and fast while still grounded.56        let context = trimmed(sample) ?? fallbackContext()5758        // 2) Grounded summarize across every catalog model.59        results += await verifyModels(catalog: catalog, context: context,60                                      quick: quick, onlyProvider: onlyProvider)6162        // 3) Core AI action matrix (against one live model per available provider).63        results += await verifyActions(catalog: catalog, context: context, onlyProvider: onlyProvider)6465        // 4) Cancellation + privacy.66        results += await verifyCancellation(catalog: catalog, context: context)6768        report(results)69        return results.contains { !$0.passed } ? 1 : 070    }7172    // MARK: - 1. Extraction suite7374    private static let extractionPages: [(name: String, url: String, selectFirstPara: Bool)] = [75        ("article",      "https://en.wikipedia.org/wiki/Cartography", false),76        ("docs",         "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch", false),77        ("long-article", "https://en.wikipedia.org/wiki/World_War_II", false),78        ("js-app",       "https://react.dev/", false),79        ("selection",    "https://en.wikipedia.org/wiki/Map", true),80    ]8182    private static func verifyExtraction() async -> ([Result], PageContext?) {83        print("— Content extraction —")84        var results: [Result] = []85        var sample: PageContext?86        let config = ProfileStore.shared.makeConfiguration(for: .defaultProfile)87        let loader = PageLoader(configuration: config)8889        for page in extractionPages {90            guard let url = URL(string: page.url) else { continue }91            let start = Date()92            do {93                try await loader.load(url)94                try? await Task.sleep(nanoseconds: 1_500_000_000)95                if page.selectFirstPara {96                    // Select the first real paragraph so the extractor captures it.97                    _ = try? await loader.webView.evaluateJavaScript(98                        "(function(){var p=Array.from(document.querySelectorAll('p')).find(function(e){return e.textContent.trim().length>80});if(!p)return false;var r=document.createRange();r.selectNodeContents(p);var s=getSelection();s.removeAllRanges();s.addRange(r);return true;})();",99                        in: nil, contentWorld: .page)100                    try? await Task.sleep(nanoseconds: 400_000_000)101                }102                let ctx = try await ContentExtractor.extract(from: loader.webView)103                let ok: Bool104                let detail: String105                if page.selectFirstPara {106                    ok = (ctx.selection?.text.isEmpty == false)107                    detail = "selection=\(ctx.selection?.text.prefix(40).description ?? "nil"), words=\(ctx.wordCount)"108                } else {109                    ok = ctx.wordCount > 80 && !ctx.markdown.isEmpty110                    detail = "quality=\(ctx.quality.rawValue), words=\(ctx.wordCount), ~\(ctx.estimatedTokens)tok, headings=\(ctx.headings.count)"111                }112                results.append(Result(section: "Extraction", provider: "—", subject: page.name,113                                      test: "extract", passed: ok,114                                      latency: Date().timeIntervalSince(start), detail: detail))115                if page.name == "article" { sample = ctx }116                print("  \(ok ? "✓" : "✗") \(page.name): \(detail)")117            } catch {118                results.append(Result(section: "Extraction", provider: "—", subject: page.name,119                                      test: "extract", passed: false, latency: nil,120                                      detail: error.localizedDescription))121                print("  ✗ \(page.name): \(error.localizedDescription)")122            }123        }124        return (results, sample)125    }126127    // MARK: - 2. Model sweep (grounded summarize on every model)128129    private static func verifyModels(catalog: ModelCatalog, context: PageContext,130                                     quick: Bool, onlyProvider: ProviderID?) async -> [Result] {131        print("\n— AI summarize across all catalog models —")132        var results: [Result] = []133        let providers = ProviderID.builtIn.filter { onlyProvider == nil || $0 == onlyProvider }134135        for provider in providers {136            guard let key = apiKey(for: provider) else {137                print("  ⚠︎ \(provider.rawValue): no key — skipped"); continue138            }139            var models = catalog.models(for: provider)140            if quick { models = Array(models.prefix(1)) }141            let serial = provider == .cerebras || provider == .mistral142            if serial {143                for model in models {144                    results.append(await summarizeTest(model: model, context: context, key: key))145                    // Cerebras free tier: 5 req/min.146                    try? await Task.sleep(nanoseconds: provider == .cerebras ? 13_000_000_000 : 1_100_000_000)147                }148            } else {149                results += await limitedConcurrent(models, limit: 3) { model in150                    await summarizeTest(model: model, context: context, key: key)151                }152            }153            let pass = results.filter { $0.section == "Models" && $0.provider == provider.displayName && $0.passed }.count154            let total = results.filter { $0.section == "Models" && $0.provider == provider.displayName }.count155            print("  \(pass == total ? "✓" : "✗") \(provider.displayName): \(pass)/\(total) models")156        }157        return results158    }159160    private static func summarizeTest(model: AIModel, context: PageContext, key: String) async -> Result {161        let start = Date()162        let client = ProviderRegistry.client(for: model)163        // Reasoning models stream hidden thinking first; give them room to reach text.164        var params = ChatParameters(maxTokens: model.capabilities.reasoning ? 8000 : 256)165        if model.parameterSupport.reasoningEffort { params.reasoningEffort = "low" }166        let request = ChatRequest(167            model: model, systemPrompt: AIAction.summarize.systemGuidance,168            messages: [Message(role: .user, text: AIAction.summarize.userPrompt(for: context))],169            parameters: params)170        do {171            var deltas = 0, text = ""172            for try await event in client.streamChat(request, apiKey: key) {173                if case .textDelta(let d) = event { deltas += 1; text += d }174            }175            let ok = deltas >= 1 && !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty176            return Result(section: "Models", provider: model.provider.displayName, subject: model.id,177                          test: "summarize", passed: ok, latency: Date().timeIntervalSince(start),178                          detail: ok ? "deltas=\(deltas)" : "empty response")179        } catch {180            return Result(section: "Models", provider: model.provider.displayName, subject: model.id,181                          test: "summarize", passed: false, latency: Date().timeIntervalSince(start),182                          detail: error.localizedDescription)183        }184    }185186    // MARK: - 3. Action matrix187188    private static func verifyActions(catalog: ModelCatalog, context: PageContext,189                                      onlyProvider: ProviderID?) async -> [Result] {190        print("\n— Core AI action matrix —")191        // Pick the first provider with a key (or the requested one).192        let providers = ProviderID.builtIn.filter { onlyProvider == nil || $0 == onlyProvider }193        guard let provider = providers.first(where: { apiKey(for: $0) != nil }),194              let model = catalog.cheapestModel(for: provider) ?? catalog.models(for: provider).first,195              let key = apiKey(for: provider) else {196            print("  ⚠︎ no keyed provider for action matrix"); return []197        }198199        var results: [Result] = []200        func record(_ name: String, _ system: String?, _ user: String) async {201            let start = Date()202            let client = ProviderRegistry.client(for: model)203            var p = ChatParameters(maxTokens: 256)204            if model.parameterSupport.reasoningEffort { p.reasoningEffort = "low" }205            let req = ChatRequest(model: model, systemPrompt: system,206                                  messages: [Message(role: .user, text: user)], parameters: p)207            do {208                var text = ""209                for try await ev in client.streamChat(req, apiKey: key) {210                    if case .textDelta(let d) = ev { text += d }211                }212                let ok = !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty213                results.append(Result(section: "Actions", provider: model.provider.displayName,214                                      subject: name, test: "action", passed: ok,215                                      latency: Date().timeIntervalSince(start),216                                      detail: ok ? "" : "empty"))217                print("  \(ok ? "✓" : "✗") \(name)")218            } catch {219                results.append(Result(section: "Actions", provider: model.provider.displayName,220                                      subject: name, test: "action", passed: false, latency: nil,221                                      detail: error.localizedDescription))222                print("  ✗ \(name): \(error.localizedDescription)")223            }224        }225226        let selText = "Cartography is the study and practice of making and using maps."227        await record("omnibox-ask", "You answer grounded in the page.",228                     AIAction.ask.userPrompt(for: context, extra: "What is this page about?"))229        await record("summarize", AIAction.summarize.systemGuidance, AIAction.summarize.userPrompt(for: context))230        await record("chat-followup", AIAction.ask.systemGuidance,231                     AIAction.ask.userPrompt(for: context, extra: "List two key facts."))232        await record("selection-explain", AIAction.explainSelection.selectionPrompt(selText).system,233                     AIAction.explainSelection.selectionPrompt(selText).user)234        await record("selection-translate", AIAction.translate.selectionPrompt(selText, language: "French").system,235                     AIAction.translate.selectionPrompt(selText, language: "French").user)236        await record("selection-rewrite", AIAction.rewrite.selectionPrompt(selText).system,237                     AIAction.rewrite.selectionPrompt(selText).user)238        await record("multi-tab-compare", "You compare multiple web pages.",239                     "Compare these two pages briefly:\n\nA) \(context.title)\n\(context.markdown.prefix(800))\n\nB) A blank page.")240        return results241    }242243    // MARK: - 4. Cancellation + privacy244245    private static func verifyCancellation(catalog: ModelCatalog, context: PageContext) async -> [Result] {246        print("\n— Cancellation & privacy —")247        guard let provider = ProviderID.builtIn.first(where: { apiKey(for: $0) != nil }),248              let model = catalog.cheapestModel(for: provider) ?? catalog.models(for: provider).first,249              let key = apiKey(for: provider) else { return [] }250251        let service = AIService()252        service.run(.summarize, on: context, model: model, apiKey: key)253        try? await Task.sleep(nanoseconds: 300_000_000)254        service.cancel()255        try? await Task.sleep(nanoseconds: 400_000_000)256        let stopped = !service.isStreaming257        print("  \(stopped ? "✓" : "✗") cancel-on-navigation stops the stream")258259        // Privacy invariant: a fresh service sends nothing until run() is called.260        let idle = AIService()261        let noSend = idle.output.isEmpty && !idle.isStreaming262        print("  \(noSend ? "✓" : "✗") no content leaves the device without a user action")263264        return [265            Result(section: "Privacy", provider: "—", subject: "cancel-on-navigation", test: "cancel",266                   passed: stopped, latency: nil, detail: stopped ? "stream cancelled" : "still streaming"),267            Result(section: "Privacy", provider: "—", subject: "no-send-without-action", test: "privacy",268                   passed: noSend, latency: nil, detail: "AIService idle until run()"),269        ]270    }271272    // MARK: - Reporting273274    private static func report(_ results: [Result]) {275        let fails = results.filter { !$0.passed }276        var lines = ["<!--", "  VERIFICATION.md", "  Zyquo Atlas", "",277                     "  Author: Simon-Pierre Boucher", "  Mail: contact@spboucher.ai", "-->", "",278                     "# Zyquo Atlas — Phase 7 Verification",279                     "",280                     "**\(results.count) checks · \(results.count - fails.count) passed · \(fails.count) failed**",281                     "",282                     "Exercises the Atlas stack (ContentExtractor + AIService + ported provider clients)",283                     "against live APIs with real keys. Page content is sent only on an AI action; keys",284                     "come from the environment and are never logged.", ""]285286        func table(_ section: String, cols: [String]) {287            let rows = results.filter { $0.section == section }288            guard !rows.isEmpty else { return }289            lines.append("## \(section)")290            lines.append("")291            lines.append("| " + cols.joined(separator: " | ") + " |")292            lines.append("|" + cols.map { _ in "---" }.joined(separator: "|") + "|")293            for r in rows.sorted(by: { ($0.provider, $0.subject) < ($1.provider, $1.subject) }) {294                let lat = r.latency.map { String(format: "%.1fs", $0) } ?? "—"295                let d = r.detail.replacingOccurrences(of: "|", with: "\\|")296                lines.append("| \(r.provider) | `\(r.subject)` | \(r.test) | \(r.passed ? "✅" : "❌") | \(lat) | \(d) |")297            }298            lines.append("")299        }300        table("Extraction", cols: ["Source", "Page", "Test", "Result", "Latency", "Detail"])301        table("Models", cols: ["Provider", "Model", "Test", "Result", "Latency", "Detail"])302        table("Actions", cols: ["Provider", "Action", "Test", "Result", "Latency", "Detail"])303        table("Privacy", cols: ["—", "Check", "Test", "Result", "Latency", "Detail"])304305        let doc = lines.joined(separator: "\n")306        let url = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)307            .appendingPathComponent("docs/VERIFICATION.md")308        try? doc.data(using: .utf8)?.write(to: url)309310        print("\n" + String(repeating: "—", count: 64))311        for f in fails { print("❌ [\(f.section)] \(f.provider) \(f.subject) [\(f.test)] — \(f.detail)") }312        print(String(repeating: "—", count: 64))313        print("\(results.count) checks · \(results.count - fails.count) passed · \(fails.count) failed")314        print("Full table: docs/VERIFICATION.md")315    }316317    // MARK: - Vault seeding318319    static func loadVault() {320        let store = SecureKeyStore()321        var loaded: [String] = []322        for (provider, name) in environmentKeys {323            if let key = ProcessInfo.processInfo.environment[name], !key.isEmpty {324                try? store.setKey(key, for: provider)325                loaded.append(provider.rawValue)326            }327        }328        print("Vault updated with keys for: \(loaded.sorted().joined(separator: ", "))")329    }330331    // MARK: - Helpers332333    static func apiKey(for provider: ProviderID) -> String? {334        guard let name = environmentKeys[provider] else { return nil }335        let value = ProcessInfo.processInfo.environment[name]336        return (value?.isEmpty ?? true) ? nil : value337    }338339    private static func value(after flag: String, in args: [String]) -> String? {340        guard let i = args.firstIndex(of: flag), i + 1 < args.count else { return nil }341        return args[i + 1]342    }343344    /// Trims a PageContext's markdown so 170 model calls stay cheap but grounded.345    private static func trimmed(_ ctx: PageContext?) -> PageContext? {346        guard let ctx, !ctx.markdown.isEmpty else { return nil }347        let short = String(ctx.markdown.prefix(1800))348        return PageContext(cloning: ctx, markdown: short)349    }350351    /// A baked context if extraction failed entirely (keeps the sweep runnable).352    private static func fallbackContext() -> PageContext {353        PageContext(bakedTitle: "Cartography",354                    url: "https://en.wikipedia.org/wiki/Cartography",355                    markdown: "# Cartography\n\nCartography is the study and practice of making and using maps. Combining science, aesthetics and technique, it builds on the premise that reality can be modeled in ways that communicate spatial information effectively.")356    }357358    private static func limitedConcurrent<T: Sendable>(359        _ items: [AIModel], limit: Int,360        _ op: @escaping @Sendable (AIModel) async -> T361    ) async -> [T] {362        await withTaskGroup(of: (Int, T).self) { group in363            var out: [(Int, T)] = []364            var it = items.enumerated().makeIterator()365            var inFlight = 0366            func addNext() { if let (i, m) = it.next() { inFlight += 1; group.addTask { (i, await op(m)) } } }367            for _ in 0..<limit { addNext() }368            while inFlight > 0 {369                if let r = await group.next() { out.append(r); inFlight -= 1; addNext() }370            }371            return out.sorted { $0.0 < $1.0 }.map { $0.1 }372        }373    }374}375