feat: bootstrap Metrika v0.1 skeleton with working ZQL vertical slice
- MetrikaKit SPM package: ZQParser, ZQPlanner, ZQEngine, ZQData, ZQStats, ZQGPU, ZQGraphics, ZQPlugins (Swift 6, strict concurrency) - ZQL parser: lexer, Pratt expressions, factor variables, prefix commands, column-cited errors with Levenshtein verb suggestions - ZQData: DuckDB-backed load/save (parquet, csv, json, arrow) - ZQStats: OLS via LAPACK QR, HC0-HC3 and cluster-robust SE, summarize, t/F distributions accurate in the far tails - ZQGPU: Philox4x32-10 reference RNG, counter-addressable bootstrap - ZQEngine: session actor with use/save/gen/replace/drop/keep/summarize/ regress/count/list/graph/bootstrap/set seed/xtset/log - SwiftUI app (xcodegen): console with history, variables sidebar, Swift Charts plots; sandboxed + hardened runtime entitlements - Tests: 44 green (parser golden, R fixtures at 1e-10, Philox KAT, end-to-end engine); Tests/Fixtures/generate.R; Tests/Bench harness - scripts: check_headers.sh + pre-commit hook, make_icns.sh, release.sh Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69 changed files +6,758 −0
added
.gitignore
+22 −0
@@ -0,0 +1,22 @@ | ||
| 1 | +# macOS | |
| 2 | +.DS_Store | |
| 3 | + | |
| 4 | +# Xcode | |
| 5 | +build/ | |
| 6 | +DerivedData/ | |
| 7 | +*.xcuserstate | |
| 8 | +xcuserdata/ | |
| 9 | +*.xcscmblueprint | |
| 10 | +*.xccheckout | |
| 11 | + | |
| 12 | +# SPM | |
| 13 | +.build/ | |
| 14 | +.swiftpm/ | |
| 15 | + | |
| 16 | +# Generated artifacts | |
| 17 | +*.icns | |
| 18 | +*.dmg | |
| 19 | + | |
| 20 | +# Local credentials (never commit) | |
| 21 | +Metrika-local/ | |
| 22 | +Zyquo-local/ | |
added
Assets/AppIcon/metrika-icon.svg
+131 −0
@@ -0,0 +1,131 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!-- | |
| 3 | + metrika-icon.svg — Metrika | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 7 | +--> | |
| 8 | +<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"> | |
| 9 | + <defs> | |
| 10 | + <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| 11 | + <stop offset="0%" stop-color="#0B1026"/> | |
| 12 | + <stop offset="45%" stop-color="#141B3D"/> | |
| 13 | + <stop offset="100%" stop-color="#1E0B38"/> | |
| 14 | + </linearGradient> | |
| 15 | + <radialGradient id="glow" cx="50%" cy="52%" r="55%"> | |
| 16 | + <stop offset="0%" stop-color="#7C5CFF" stop-opacity="0.55"/> | |
| 17 | + <stop offset="55%" stop-color="#4A3AC8" stop-opacity="0.18"/> | |
| 18 | + <stop offset="100%" stop-color="#000000" stop-opacity="0"/> | |
| 19 | + </radialGradient> | |
| 20 | + <linearGradient id="mstroke" x1="0%" y1="0%" x2="100%" y2="0%"> | |
| 21 | + <stop offset="0%" stop-color="#3EF2D4"/> | |
| 22 | + <stop offset="50%" stop-color="#7C5CFF"/> | |
| 23 | + <stop offset="100%" stop-color="#FF4FA3"/> | |
| 24 | + </linearGradient> | |
| 25 | + <linearGradient id="mcore" x1="0%" y1="0%" x2="100%" y2="0%"> | |
| 26 | + <stop offset="0%" stop-color="#EAFFFB"/> | |
| 27 | + <stop offset="50%" stop-color="#E4DBFF"/> | |
| 28 | + <stop offset="100%" stop-color="#FFE1F0"/> | |
| 29 | + </linearGradient> | |
| 30 | + <linearGradient id="area" x1="0%" y1="0%" x2="0%" y2="100%"> | |
| 31 | + <stop offset="0%" stop-color="#7C5CFF" stop-opacity="0.30"/> | |
| 32 | + <stop offset="100%" stop-color="#7C5CFF" stop-opacity="0"/> | |
| 33 | + </linearGradient> | |
| 34 | + <filter id="blurBig" x="-40%" y="-40%" width="180%" height="180%"> | |
| 35 | + <feGaussianBlur stdDeviation="26"/> | |
| 36 | + </filter> | |
| 37 | + <filter id="blurSoft" x="-40%" y="-40%" width="180%" height="180%"> | |
| 38 | + <feGaussianBlur stdDeviation="7"/> | |
| 39 | + </filter> | |
| 40 | + <linearGradient id="sheen" x1="0%" y1="0%" x2="0%" y2="100%"> | |
| 41 | + <stop offset="0%" stop-color="#FFFFFF" stop-opacity="0.16"/> | |
| 42 | + <stop offset="35%" stop-color="#FFFFFF" stop-opacity="0.04"/> | |
| 43 | + <stop offset="60%" stop-color="#FFFFFF" stop-opacity="0"/> | |
| 44 | + </linearGradient> | |
| 45 | + <clipPath id="squircle"> | |
| 46 | + <rect x="64" y="64" width="896" height="896" rx="200"/> | |
| 47 | + </clipPath> | |
| 48 | + </defs> | |
| 49 | + | |
| 50 | + <rect x="64" y="64" width="896" height="896" rx="200" fill="url(#bg)"/> | |
| 51 | + | |
| 52 | + <g clip-path="url(#squircle)"> | |
| 53 | + <!-- Plot grid --> | |
| 54 | + <g stroke="#8FA3FF" stroke-opacity="0.07" stroke-width="2"> | |
| 55 | + <line x1="64" y1="288" x2="960" y2="288"/> | |
| 56 | + <line x1="64" y1="464" x2="960" y2="464"/> | |
| 57 | + <line x1="64" y1="640" x2="960" y2="640"/> | |
| 58 | + <line x1="64" y1="816" x2="960" y2="816"/> | |
| 59 | + <line x1="256" y1="64" x2="256" y2="960"/> | |
| 60 | + <line x1="432" y1="64" x2="432" y2="960"/> | |
| 61 | + <line x1="608" y1="64" x2="608" y2="960"/> | |
| 62 | + <line x1="784" y1="64" x2="784" y2="960"/> | |
| 63 | + </g> | |
| 64 | + | |
| 65 | + <rect x="64" y="64" width="896" height="896" fill="url(#glow)"/> | |
| 66 | + | |
| 67 | + <!-- Area fill under the M trace (chart vibe) --> | |
| 68 | + <path d="M 220 800 L 220 300 L 420 640 L 512 380 L 604 640 L 804 300 L 804 800 Z" | |
| 69 | + fill="url(#area)"/> | |
| 70 | + | |
| 71 | + <!-- Scatter cloud around the trace --> | |
| 72 | + <g fill="#9DB4FF" fill-opacity="0.55"> | |
| 73 | + <circle cx="286" cy="418" r="9"/> | |
| 74 | + <circle cx="330" cy="512" r="7"/> | |
| 75 | + <circle cx="378" cy="566" r="10"/> | |
| 76 | + <circle cx="452" cy="536" r="8"/> | |
| 77 | + <circle cx="486" cy="446" r="7"/> | |
| 78 | + <circle cx="542" cy="452" r="9"/> | |
| 79 | + <circle cx="580" cy="560" r="8"/> | |
| 80 | + <circle cx="646" cy="576" r="10"/> | |
| 81 | + <circle cx="700" cy="470" r="7"/> | |
| 82 | + <circle cx="748" cy="392" r="9"/> | |
| 83 | + <circle cx="264" cy="640" r="7"/> | |
| 84 | + <circle cx="760" cy="640" r="8"/> | |
| 85 | + </g> | |
| 86 | + <g fill="#3EF2D4" fill-opacity="0.85"> | |
| 87 | + <circle cx="252" cy="356" r="6"/> | |
| 88 | + <circle cx="470" cy="410" r="6"/> | |
| 89 | + <circle cx="676" cy="520" r="6"/> | |
| 90 | + </g> | |
| 91 | + <g fill="#FF4FA3" fill-opacity="0.8"> | |
| 92 | + <circle cx="356" cy="600" r="6"/> | |
| 93 | + <circle cx="556" cy="500" r="7"/> | |
| 94 | + <circle cx="772" cy="340" r="6"/> | |
| 95 | + </g> | |
| 96 | + | |
| 97 | + <!-- The M : time-series trace --> | |
| 98 | + <path d="M 220 800 L 220 300 L 420 640 L 512 380 L 604 640 L 804 300 L 804 800" | |
| 99 | + fill="none" stroke="url(#mstroke)" stroke-width="96" | |
| 100 | + stroke-linecap="round" stroke-linejoin="round" | |
| 101 | + filter="url(#blurBig)" opacity="0.75"/> | |
| 102 | + <path d="M 220 800 L 220 300 L 420 640 L 512 380 L 604 640 L 804 300 L 804 800" | |
| 103 | + fill="none" stroke="url(#mstroke)" stroke-width="74" | |
| 104 | + stroke-linecap="round" stroke-linejoin="round"/> | |
| 105 | + <path d="M 220 800 L 220 300 L 420 640 L 512 380 L 604 640 L 804 300 L 804 800" | |
| 106 | + fill="none" stroke="url(#mcore)" stroke-width="20" | |
| 107 | + stroke-linecap="round" stroke-linejoin="round" | |
| 108 | + filter="url(#blurSoft)" opacity="0.9"/> | |
| 109 | + | |
| 110 | + <!-- Data-point nodes at the vertices of the trace --> | |
| 111 | + <g fill="#FFFFFF"> | |
| 112 | + <circle cx="220" cy="300" r="16"/> | |
| 113 | + <circle cx="420" cy="640" r="16"/> | |
| 114 | + <circle cx="512" cy="380" r="16"/> | |
| 115 | + <circle cx="604" cy="640" r="16"/> | |
| 116 | + <circle cx="804" cy="300" r="16"/> | |
| 117 | + </g> | |
| 118 | + <g fill="none" stroke="#FFFFFF" stroke-opacity="0.35" stroke-width="6"> | |
| 119 | + <circle cx="220" cy="300" r="30"/> | |
| 120 | + <circle cx="420" cy="640" r="30"/> | |
| 121 | + <circle cx="512" cy="380" r="30"/> | |
| 122 | + <circle cx="604" cy="640" r="30"/> | |
| 123 | + <circle cx="804" cy="300" r="30"/> | |
| 124 | + </g> | |
| 125 | + | |
| 126 | + <rect x="64" y="64" width="896" height="520" fill="url(#sheen)"/> | |
| 127 | + </g> | |
| 128 | + | |
| 129 | + <rect x="66" y="66" width="892" height="892" rx="198" fill="none" | |
| 130 | + stroke="#FFFFFF" stroke-opacity="0.10" stroke-width="3"/> | |
| 131 | +</svg> | |
added
CLAUDE.md
+374 −0
@@ -0,0 +1,374 @@ | ||
| 1 | +# CLAUDE.md — Metrika | |
| 2 | + | |
| 3 | +> **Metrika** — A next-generation statistical & data science application for macOS. | |
| 4 | +> Stata-class command syntax, GPU-accelerated by Apple Silicon (MLX/Metal), powered by DuckDB. | |
| 5 | +> Native Swift + SwiftUI. No Electron. No Python runtime dependency. No compromises. | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## 0. Non-Negotiable Project Rules | |
| 10 | + | |
| 11 | +1. **Every source file MUST begin with this header** (adapt comment style to the language): | |
| 12 | + | |
| 13 | +```swift | |
| 14 | +// | |
| 15 | +// <FileName>.swift | |
| 16 | +// Metrika | |
| 17 | +// | |
| 18 | +// Author: Simon-Pierre Boucher | |
| 19 | +// Contact: contact@spboucher.ai | |
| 20 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 21 | +// | |
| 22 | +``` | |
| 23 | + | |
| 24 | +For Metal shaders (`.metal`), shell scripts, and Python tooling scripts, use the equivalent comment syntax (`//` or `#`). **No file is committed without this header.** A pre-commit hook (see §12) enforces it. | |
| 25 | + | |
| 26 | +2. Language: **Swift 6**, strict concurrency enabled (`-strict-concurrency=complete`). | |
| 27 | +3. UI: **SwiftUI first**, AppKit bridges (`NSViewRepresentable`) only where SwiftUI is insufficient (text editor, data grid virtualization). | |
| 28 | +4. Minimum target: **macOS 14 (Sonoma)**, Apple Silicon only (`arm64`). No Intel builds. | |
| 29 | +5. All numerical results must match reference implementations (R / Stata) to at least 1e-10 relative tolerance for CPU paths; GPU paths documented tolerance 1e-6 (float32) or 1e-10 (float64 where MLX supports it). | |
| 30 | +6. English for all code, comments, commit messages, and docs. | |
| 31 | + | |
| 32 | +--- | |
| 33 | + | |
| 34 | +## 1. Product Vision | |
| 35 | + | |
| 36 | +Metrika is a complete data science environment: | |
| 37 | + | |
| 38 | +- **Command-driven**, like Stata: `reg y x1 x2, robust cluster(id)` — one line, publication-ready output. | |
| 39 | +- **Scriptable**: `.zyq` do-files with full reproducibility (logs, seeds, versioned datasets). | |
| 40 | +- **GPU-native**: massively parallel workloads (bootstrap, Monte Carlo, MCMC, ML training, permutation tests) run on Apple GPU via MLX with unified memory. | |
| 41 | +- **Extensible**: user-defined commands ("home-made" commands) via a plugin protocol — drop a Swift package or a `.zyq` ado-style script into `~/Library/Application Support/Metrika/Commands/`. | |
| 42 | +- **Big-data capable**: DuckDB engine → out-of-core, columnar, multi-threaded. 100M+ rows on a laptop. | |
| 43 | + | |
| 44 | +### Feature pillars (v1 scope) | |
| 45 | + | |
| 46 | +| Pillar | Contents | | |
| 47 | +|---|---| | |
| 48 | +| Data management | import/export CSV, Parquet, Arrow, Excel, Stata `.dta`, JSON; merge, append, reshape, collapse, egen-style generators | | |
| 49 | +| Estimation | summarize, tabulate, correlate, `regress` (OLS), `logit`, `probit`, `poisson`, `ivregress` (2SLS), `xtreg` (FE/RE), `areg`, quantile regression | | |
| 50 | +| Inference | robust (HC0–HC3), cluster-robust SE, **GPU bootstrap** (pairs, wild, cluster), permutation tests, jackknife | | |
| 51 | +| Simulation | Monte Carlo engine, random number generation (Philox counter-based, reproducible on GPU) | | |
| 52 | +| ML | gradient boosting, elastic net, k-means, PCA — MLX-backed | | |
| 53 | +| Graphics | scatter, line, histogram, kdensity, coefplot, binscatter — Swift Charts + custom Metal renderer for >1M points | | |
| 54 | +| Reproducibility | do-file editor, logging (`log using`), `set seed` honored across CPU & GPU | | |
| 55 | + | |
| 56 | +--- | |
| 57 | + | |
| 58 | +## 2. App Name, Bundle & Identity | |
| 59 | + | |
| 60 | +- **App name:** Metrika | |
| 61 | +- **Bundle ID:** `ai.spboucher.metrika` | |
| 62 | +- **Version scheme:** SemVer (`MARKETING_VERSION`), build number = CI run number. | |
| 63 | +- **Icon:** `Assets/AppIcon/metrika-icon.svg` is the master (see §11). Rasterize to all required sizes via `scripts/make_icns.sh`. | |
| 64 | + | |
| 65 | +--- | |
| 66 | + | |
| 67 | +## 3. Architecture Overview | |
| 68 | + | |
| 69 | +``` | |
| 70 | +┌─────────────────────────────────────────────────────────┐ | |
| 71 | +│ MetrikaApp (SwiftUI) │ | |
| 72 | +│ CommandConsole · DoFileEditor · DataBrowser · Plots │ | |
| 73 | +└──────────────────────────┬──────────────────────────────┘ | |
| 74 | + │ | |
| 75 | +┌──────────────────────────▼──────────────────────────────┐ | |
| 76 | +│ MetrikaKit (SPM package) │ | |
| 77 | +│ │ | |
| 78 | +│ ZQParser command grammar → AST │ | |
| 79 | +│ ZQPlanner AST → ExecutionPlan (CPU/GPU/hybrid) │ | |
| 80 | +│ ZQEngine orchestration, sessions, logging │ | |
| 81 | +│ ZQData DataFrame façade over DuckDB/Arrow │ | |
| 82 | +│ ZQStats CPU estimators (Accelerate/LAPACK) │ | |
| 83 | +│ ZQGPU MLX batched estimators, RNG, kernels │ | |
| 84 | +│ ZQGraphics plot spec → Swift Charts / Metal │ | |
| 85 | +│ ZQPlugins user command discovery & sandboxing │ | |
| 86 | +└─────────────────────────────────────────────────────────┘ | |
| 87 | +``` | |
| 88 | + | |
| 89 | +### Module rules | |
| 90 | +- `MetrikaKit` is a **Swift Package** with zero UI dependencies → fully testable via `swift test`. | |
| 91 | +- The app target only imports `MetrikaKit` and renders. | |
| 92 | +- `ZQGPU` is the **only** module allowed to import MLX/Metal. `ZQStats` is the only one importing Accelerate. This keeps backends swappable. | |
| 93 | + | |
| 94 | +### Dependencies (SPM) | |
| 95 | +``` | |
| 96 | +duckdb/duckdb-swift — data engine | |
| 97 | +ml-explore/mlx-swift — GPU tensor compute | |
| 98 | +apple/swift-collections — deques, ordered dicts | |
| 99 | +pointfreeco/swift-parsing — command grammar | |
| 100 | +apple/swift-argument-parser — metrika-cli companion tool | |
| 101 | +``` | |
| 102 | + | |
| 103 | +--- | |
| 104 | + | |
| 105 | +## 4. The Command Language (ZQL) | |
| 106 | + | |
| 107 | +Stata-compatible mental model, cleaner grammar: | |
| 108 | + | |
| 109 | +``` | |
| 110 | +command [varlist] [if expr] [in range] [weight] [, options] | |
| 111 | +``` | |
| 112 | + | |
| 113 | +Examples that MUST work in v1: | |
| 114 | + | |
| 115 | +``` | |
| 116 | +use sales.parquet | |
| 117 | +summarize revenue price, detail | |
| 118 | +gen log_rev = ln(revenue) | |
| 119 | +reg log_rev price i.region, robust | |
| 120 | +bootstrap, reps(100000) seed(42): reg log_rev price | |
| 121 | +xtset firm_id year | |
| 122 | +xtreg log_rev price, fe cluster(firm_id) | |
| 123 | +graph scatter log_rev price, by(region) | |
| 124 | +``` | |
| 125 | + | |
| 126 | +### Parser (`ZQParser`) | |
| 127 | +- Built with `swift-parsing`; grammar defined in `Sources/ZQParser/Grammar/`. | |
| 128 | +- Produces a typed AST: `ZQCommand { verb, varlist, condition, range, weight, options }`. | |
| 129 | +- Factor variables (`i.region`, `c.age#c.age`) expand at plan time, not parse time. | |
| 130 | +- Error messages must cite column position and suggest fixes (Levenshtein on known verbs). | |
| 131 | + | |
| 132 | +### Home-made commands | |
| 133 | +Two tiers: | |
| 134 | +1. **Script commands** (`.zyq` files): sequences of ZQL with `args`/`syntax` declarations — the Stata ado-file analog. | |
| 135 | +2. **Native plugins**: Swift packages conforming to: | |
| 136 | + | |
| 137 | +```swift | |
| 138 | +public protocol ZQCommandPlugin: Sendable { | |
| 139 | + static var verb: String { get } | |
| 140 | + static var syntax: ZQSyntaxSpec { get } | |
| 141 | + func execute(_ ctx: ZQContext) async throws -> ZQResult | |
| 142 | +} | |
| 143 | +``` | |
| 144 | + | |
| 145 | +Discovery path: `~/Library/Application Support/Metrika/Commands/`. Plugins run in-process but receive a **read-only DataFrame view** unless they declare `mutates: true`. | |
| 146 | + | |
| 147 | +--- | |
| 148 | + | |
| 149 | +## 5. Execution Planner — CPU vs GPU Dispatch | |
| 150 | + | |
| 151 | +The planner decides the backend **automatically**. The user never chooses. | |
| 152 | + | |
| 153 | +```swift | |
| 154 | +enum Backend { case cpu, gpu, hybrid } | |
| 155 | + | |
| 156 | +func plan(_ cmd: ZQCommand, data: ZQDataFrame) -> ExecutionPlan { | |
| 157 | + // Heuristics, tuned by benchmarks in Tests/Bench: | |
| 158 | + // 1. Single estimation, n < 5_000_000 → .cpu (LAPACK QR, dqrls-style) | |
| 159 | + // 2. reps ≥ 500 independent replicates → .gpu (batched solve) | |
| 160 | + // 3. MCMC / iterative + large n → .hybrid (GPU likelihood, CPU control) | |
| 161 | +} | |
| 162 | +``` | |
| 163 | + | |
| 164 | +### CPU path (`ZQStats`) | |
| 165 | +- OLS: QR via LAPACK `dgeqrf`/`dormqr` (never form X'X for conditioning reasons). | |
| 166 | +- Robust/cluster SE: sandwich estimators, computed with BLAS level-3. | |
| 167 | +- MLE (logit/probit/poisson): Newton–Raphson with analytic Hessians; IRLS fallback. | |
| 168 | +- All CPU compute in **Float64**. | |
| 169 | + | |
| 170 | +### GPU path (`ZQGPU`, MLX) | |
| 171 | +- **Batched OLS**: resample indices as `(reps, n)` int tensor → gather → batched Cholesky/QR solve on `(reps, k, k)` — thousands of regressions per GPU pass. | |
| 172 | +- **RNG**: Philox4x32 counter-based generator → identical streams CPU/GPU given same seed; `set seed` maps to Philox key. Reproducibility is a release blocker. | |
| 173 | +- **Cluster bootstrap** (ragged n per replicate): pad-to-max with weight masks; custom Metal kernel only if profiling shows >2× overhead from padding. | |
| 174 | +- Precision: MLX float32 by default; accumulate cross-products in float64 emulation (Kahan/two-sum) where it matters; document tolerances per command. | |
| 175 | +- **Chunk reps** to respect memory: `chunk = min(reps, budget / (n*k*4))`, stream results, report progress to UI via `AsyncStream`. | |
| 176 | + | |
| 177 | +### Hybrid | |
| 178 | +- Example: Bayesian regression — GPU computes log-likelihood over chains in parallel, CPU runs the sampler control loop. | |
| 179 | + | |
| 180 | +--- | |
| 181 | + | |
| 182 | +## 6. Data Layer (`ZQData`) | |
| 183 | + | |
| 184 | +- Canonical store: **DuckDB** in-process database; DataFrames are Arrow views over query results. | |
| 185 | +- Zero-copy handoff Arrow → MLX tensors where dtype allows; otherwise single cast pass. | |
| 186 | +- `use file.parquet` → registers a DuckDB view, lazy. `gen`/`replace` compile to SQL expressions when possible; fall back to vectorized Swift over Arrow buffers. | |
| 187 | +- Missing values: Arrow validity bitmaps end-to-end; every estimator does listwise deletion by default with an explicit report line ("(12 observations dropped due to missing values)"). | |
| 188 | +- `.dta` reader/writer implemented natively in `ZQData/Stata/` (spec v117–v121). | |
| 189 | + | |
| 190 | +--- | |
| 191 | + | |
| 192 | +## 7. UI (SwiftUI) | |
| 193 | + | |
| 194 | +Windows/panes: | |
| 195 | +1. **Console** — command input with history (↑/↓), autocomplete on verbs & varnames, ANSI-style rich results (monospaced, aligned tables via `AttributedString`). | |
| 196 | +2. **Do-file editor** — `NSTextView` bridge, syntax highlighting for ZQL, ⌘R runs selection. | |
| 197 | +3. **Data browser** — virtualized grid (custom `NSTableView` bridge; SwiftUI `Table` is too slow past ~100k rows), sortable, filter bar compiles to `if` expressions. | |
| 198 | +4. **Plots** — Swift Charts for standard plots; custom `MTKView` renderer for scatter >1M points (instanced point sprites). | |
| 199 | +5. **Sidebar** — variables list with types, labels, missing counts. | |
| 200 | + | |
| 201 | +State: `@Observable` session model; every command mutation goes through `ZQEngine` so console, do-files, and plugins share one execution path. Undo = dataset snapshots via DuckDB `CHECKPOINT` + copy-on-write. | |
| 202 | + | |
| 203 | +--- | |
| 204 | + | |
| 205 | +## 8. Performance Budgets (release blockers) | |
| 206 | + | |
| 207 | +| Operation | Data | Budget (M3 Pro) | | |
| 208 | +|---|---|---| | |
| 209 | +| `use` parquet | 10M rows × 20 cols | < 1.5 s | | |
| 210 | +| `summarize` all vars | 10M × 20 | < 300 ms | | |
| 211 | +| `reg` 5 covariates | 10M rows | < 900 ms | | |
| 212 | +| `bootstrap, reps(10000): reg` | 100k rows | < 3 s | | |
| 213 | +| `bootstrap, reps(100000): reg` | 100k rows | < 25 s | | |
| 214 | +| Scatter render | 2M points | 60 fps pan/zoom | | |
| 215 | + | |
| 216 | +Benchmarks live in `Tests/Bench/` and run in CI on self-hosted Apple Silicon runner; regressions >10% fail the build. | |
| 217 | + | |
| 218 | +--- | |
| 219 | + | |
| 220 | +## 9. Testing | |
| 221 | + | |
| 222 | +- `swift test` on MetrikaKit: parser golden tests, estimator numerical tests vs fixtures generated by R (`Tests/Fixtures/generate.R`). | |
| 223 | +- Property tests: OLS invariance (scaling, permutation), bootstrap CI coverage simulation (n small, known DGP). | |
| 224 | +- GPU vs CPU cross-checks: same seed ⇒ identical resample indices; estimates within documented tolerance. | |
| 225 | +- UI: XCUITest smoke — launch, load sample dataset, run `reg`, assert output table. | |
| 226 | + | |
| 227 | +--- | |
| 228 | + | |
| 229 | +## 10. Build, Signing, Notarization & DMG | |
| 230 | + | |
| 231 | +> ⚠️ **PLACEHOLDERS** — the `Zyquo-local/` folder containing the real notarization credentials was not available when this file was generated. Fill every `<...>` below from `Zyquo-local/notarization.md` (or equivalent) before the first release build. Do **not** commit real credentials; keep them in the local keychain / CI secrets. | |
| 232 | + | |
| 233 | +### Identifiers | |
| 234 | +``` | |
| 235 | +DEVELOPMENT_TEAM = <TEAM_ID> # from Metrika-local | |
| 236 | +CODE_SIGN_IDENTITY = "Developer ID Application: Simon-Pierre Boucher (<TEAM_ID>)" | |
| 237 | +NOTARY_PROFILE = "<KEYCHAIN_PROFILE_NAME>" # created via: xcrun notarytool store-credentials | |
| 238 | +BUNDLE_ID = ai.spboucher.metrika | |
| 239 | +``` | |
| 240 | + | |
| 241 | +### One-time setup | |
| 242 | +```bash | |
| 243 | +xcrun notarytool store-credentials "<KEYCHAIN_PROFILE_NAME>" \ | |
| 244 | + --apple-id "<APPLE_ID_EMAIL>" \ | |
| 245 | + --team-id "<TEAM_ID>" \ | |
| 246 | + --password "<APP_SPECIFIC_PASSWORD>" | |
| 247 | +``` | |
| 248 | + | |
| 249 | +### Release pipeline — `scripts/release.sh` | |
| 250 | +```bash | |
| 251 | +#!/bin/bash | |
| 252 | +# | |
| 253 | +# release.sh — Metrika | |
| 254 | +# Author: Simon-Pierre Boucher | |
| 255 | +# Contact: contact@spboucher.ai | |
| 256 | +# | |
| 257 | +set -euo pipefail | |
| 258 | + | |
| 259 | +APP="Metrika" | |
| 260 | +SCHEME="Metrika" | |
| 261 | +BUILD_DIR="build" | |
| 262 | +DMG="${APP}.dmg" | |
| 263 | + | |
| 264 | +# 1. Archive | |
| 265 | +xcodebuild -scheme "$SCHEME" -configuration Release -arch arm64 \ | |
| 266 | + -archivePath "$BUILD_DIR/$APP.xcarchive" archive | |
| 267 | + | |
| 268 | +# 2. Export with Developer ID | |
| 269 | +xcodebuild -exportArchive \ | |
| 270 | + -archivePath "$BUILD_DIR/$APP.xcarchive" \ | |
| 271 | + -exportOptionsPlist scripts/ExportOptions.plist \ | |
| 272 | + -exportPath "$BUILD_DIR/export" | |
| 273 | + | |
| 274 | +# 3. Verify hardened runtime + entitlements | |
| 275 | +codesign -dv --verbose=4 "$BUILD_DIR/export/$APP.app" | |
| 276 | +codesign --verify --deep --strict "$BUILD_DIR/export/$APP.app" | |
| 277 | + | |
| 278 | +# 4. Build DMG (create-dmg, background art in Assets/DMG/) | |
| 279 | +create-dmg \ | |
| 280 | + --volname "$APP" \ | |
| 281 | + --window-size 540 380 \ | |
| 282 | + --icon-size 128 \ | |
| 283 | + --icon "$APP.app" 130 190 \ | |
| 284 | + --app-drop-link 400 190 \ | |
| 285 | + --background "Assets/DMG/background.png" \ | |
| 286 | + "$DMG" "$BUILD_DIR/export/" | |
| 287 | + | |
| 288 | +# 5. Sign the DMG itself | |
| 289 | +codesign --sign "Developer ID Application: Simon-Pierre Boucher (<TEAM_ID>)" "$DMG" | |
| 290 | + | |
| 291 | +# 6. Notarize & wait | |
| 292 | +xcrun notarytool submit "$DMG" \ | |
| 293 | + --keychain-profile "<KEYCHAIN_PROFILE_NAME>" \ | |
| 294 | + --wait | |
| 295 | + | |
| 296 | +# 7. Staple | |
| 297 | +xcrun stapler staple "$DMG" | |
| 298 | +xcrun stapler validate "$DMG" | |
| 299 | + | |
| 300 | +echo "✅ $DMG notarized and stapled." | |
| 301 | +``` | |
| 302 | + | |
| 303 | +### `scripts/ExportOptions.plist` | |
| 304 | +```xml | |
| 305 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 306 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" | |
| 307 | + "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 308 | +<plist version="1.0"><dict> | |
| 309 | + <key>method</key><string>developer-id</string> | |
| 310 | + <key>teamID</key><string><TEAM_ID></string> | |
| 311 | + <key>signingStyle</key><string>automatic</string> | |
| 312 | + <key>destination</key><string>export</string> | |
| 313 | +</dict></plist> | |
| 314 | +``` | |
| 315 | + | |
| 316 | +### Entitlements (`Metrika.entitlements`) | |
| 317 | +- Hardened Runtime: ON. | |
| 318 | +- App Sandbox: ON, with `com.apple.security.files.user-selected.read-write` (open/save datasets) and bookmark entitlements for recent files. | |
| 319 | +- No JIT, no unsigned executable memory (MLX does not require them). | |
| 320 | +- If plugin loading of external dylibs is added later, revisit `com.apple.security.cs.disable-library-validation` — avoid if possible; prefer SPM-compiled-in plugins. | |
| 321 | + | |
| 322 | +--- | |
| 323 | + | |
| 324 | +## 11. App Icon | |
| 325 | + | |
| 326 | +- Master: `Assets/AppIcon/metrika-icon.svg` (1024×1024 design). | |
| 327 | +- Concept: a glowing **M-shaped time-series line** — a zigzag chart trace cutting through a scatter of data points on a deep space-gradient squircle — econometrics meets GPU power. | |
| 328 | +- Generate `.icns`: `scripts/make_icns.sh` rasterizes the SVG (via `rsvg-convert` or `qlmanage`) to 16→1024 px and runs `iconutil -c icns`. | |
| 329 | +- macOS Sonoma+ icon grid: content within the 824×824 safe zone of the 1024 canvas; system applies the squircle mask — but we bake our own rounded-rect background per Big Sur+ convention. | |
| 330 | + | |
| 331 | +--- | |
| 332 | + | |
| 333 | +## 12. Repo Conventions | |
| 334 | + | |
| 335 | +``` | |
| 336 | +Metrika/ | |
| 337 | +├── CLAUDE.md | |
| 338 | +├── Metrika.xcodeproj | |
| 339 | +├── Metrika/ # app target (UI) | |
| 340 | +├── MetrikaKit/ # SPM package (all logic) | |
| 341 | +│ └── Sources/{ZQParser,ZQPlanner,ZQEngine,ZQData,ZQStats,ZQGPU,ZQGraphics,ZQPlugins} | |
| 342 | +├── Assets/{AppIcon,DMG} | |
| 343 | +├── scripts/{release.sh,make_icns.sh,check_headers.sh} | |
| 344 | +└── Tests/{Unit,Bench,Fixtures} | |
| 345 | +``` | |
| 346 | + | |
| 347 | +- **Pre-commit hook** runs `scripts/check_headers.sh`: rejects any staged source file (`.swift`, `.metal`, `.sh`, `.py`, `.zyq`) missing the `Author: Simon-Pierre Boucher` / `Contact: contact@spboucher.ai` header. | |
| 348 | +- Commits: Conventional Commits (`feat(parser): factor variable expansion`). | |
| 349 | +- Branches: `main` (protected), `dev`, feature branches `feat/*`. | |
| 350 | +- CI: GitHub Actions on self-hosted arm64 macOS runner — build, test, bench, header check. | |
| 351 | + | |
| 352 | +--- | |
| 353 | + | |
| 354 | +## 13. Roadmap | |
| 355 | + | |
| 356 | +**v0.1 (MVP, ~8 weeks of focused work)** | |
| 357 | +use/save (parquet, csv, dta) · summarize · tabulate · gen/replace · reg (robust, cluster) · logit · basic scatter/histogram · console + data browser · do-file execution. | |
| 358 | + | |
| 359 | +**v0.2** — GPU bootstrap + permutation tests · xtreg FE · ivregress · Swift Charts full suite · plugin protocol. | |
| 360 | + | |
| 361 | +**v0.3** — MCMC/Bayesian module · gradient boosting & elastic net (MLX) · Metal 2M-point renderer · margins/predict. | |
| 362 | + | |
| 363 | +**v1.0** — notarized DMG release, docs site, sample datasets, command reference (`help regress` in-app). | |
| 364 | + | |
| 365 | +--- | |
| 366 | + | |
| 367 | +## 14. What Claude Should Do When Working in This Repo | |
| 368 | + | |
| 369 | +1. Read this file first; respect §0 rules absolutely (headers, Swift 6 strict concurrency). | |
| 370 | +2. Before touching numerics, read the matching fixture in `Tests/Fixtures/` and keep tests green. | |
| 371 | +3. Any new command: grammar entry → AST case → planner rule → CPU impl → (optional) GPU impl → golden test → in-app help entry. All six or the PR is incomplete. | |
| 372 | +4. Never introduce a Python or Node runtime dependency into the app bundle. | |
| 373 | +5. When unsure between CPU and GPU, implement CPU first — correctness before speed — then add the GPU path behind the planner. | |
| 374 | +6. Update the performance table (§8) whenever a benchmark materially changes. | |
added
Metrika.xcodeproj/project.pbxproj
+383 −0
@@ -0,0 +1,383 @@ | ||
| 1 | +// !$*UTF8*$! | |
| 2 | +{ | |
| 3 | + archiveVersion = 1; | |
| 4 | + classes = { | |
| 5 | + }; | |
| 6 | + objectVersion = 77; | |
| 7 | + objects = { | |
| 8 | + | |
| 9 | +/* Begin PBXBuildFile section */ | |
| 10 | + 0F7000A49C9879593F09A0D3 /* MetrikaKit in Frameworks */ = {isa = PBXBuildFile; productRef = 498C4115F45BADDF16191A57 /* MetrikaKit */; }; | |
| 11 | + 3084AE2A328ED6746D01F46E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0404FDEAF8A57414EFB3C649 /* Assets.xcassets */; }; | |
| 12 | + 7E936A23CCC90823D6B08BCC /* SessionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0453FB0F256E853D2C2F481C /* SessionModel.swift */; }; | |
| 13 | + 8B0CDB213D64E3EE87593808 /* ConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159695CF89D6BB25DF883F07 /* ConsoleView.swift */; }; | |
| 14 | + 9BE65709781719FAE0CA740A /* MetrikaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5393DC050C555E4FB69FC0 /* MetrikaApp.swift */; }; | |
| 15 | + DDCC2DC487AF9FAC7D6B8E3E /* PlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD551AA707CD1853C3D6D21 /* PlotView.swift */; }; | |
| 16 | +/* End PBXBuildFile section */ | |
| 17 | + | |
| 18 | +/* Begin PBXFileReference section */ | |
| 19 | + 0404FDEAF8A57414EFB3C649 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; | |
| 20 | + 0453FB0F256E853D2C2F481C /* SessionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionModel.swift; sourceTree = "<group>"; }; | |
| 21 | + 159695CF89D6BB25DF883F07 /* ConsoleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleView.swift; sourceTree = "<group>"; }; | |
| 22 | + 24D84DAB354D702D8575D587 /* MetrikaKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MetrikaKit; path = MetrikaKit; sourceTree = SOURCE_ROOT; }; | |
| 23 | + 4AD551AA707CD1853C3D6D21 /* PlotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlotView.swift; sourceTree = "<group>"; }; | |
| 24 | + 5A0213CC9FE05A339DEEFA28 /* Metrika.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metrika.app; sourceTree = BUILT_PRODUCTS_DIR; }; | |
| 25 | + 6E94C5817E96C83B888FB579 /* Metrika.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Metrika.entitlements; sourceTree = "<group>"; }; | |
| 26 | + 9C5393DC050C555E4FB69FC0 /* MetrikaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetrikaApp.swift; sourceTree = "<group>"; }; | |
| 27 | +/* End PBXFileReference section */ | |
| 28 | + | |
| 29 | +/* Begin PBXFrameworksBuildPhase section */ | |
| 30 | + 168283EE9449D9A4324BFE40 /* Frameworks */ = { | |
| 31 | + isa = PBXFrameworksBuildPhase; | |
| 32 | + buildActionMask = 2147483647; | |
| 33 | + files = ( | |
| 34 | + 0F7000A49C9879593F09A0D3 /* MetrikaKit in Frameworks */, | |
| 35 | + ); | |
| 36 | + runOnlyForDeploymentPostprocessing = 0; | |
| 37 | + }; | |
| 38 | +/* End PBXFrameworksBuildPhase section */ | |
| 39 | + | |
| 40 | +/* Begin PBXGroup section */ | |
| 41 | + 133558B8D49331BB5120F8F8 /* Products */ = { | |
| 42 | + isa = PBXGroup; | |
| 43 | + children = ( | |
| 44 | + 5A0213CC9FE05A339DEEFA28 /* Metrika.app */, | |
| 45 | + ); | |
| 46 | + name = Products; | |
| 47 | + sourceTree = "<group>"; | |
| 48 | + }; | |
| 49 | + 58DB3AB5F820BD1E6C82E1CB /* Sources */ = { | |
| 50 | + isa = PBXGroup; | |
| 51 | + children = ( | |
| 52 | + 159695CF89D6BB25DF883F07 /* ConsoleView.swift */, | |
| 53 | + 9C5393DC050C555E4FB69FC0 /* MetrikaApp.swift */, | |
| 54 | + 4AD551AA707CD1853C3D6D21 /* PlotView.swift */, | |
| 55 | + 0453FB0F256E853D2C2F481C /* SessionModel.swift */, | |
| 56 | + ); | |
| 57 | + path = Sources; | |
| 58 | + sourceTree = "<group>"; | |
| 59 | + }; | |
| 60 | + 63F77F28B4EF42E0407BEC4F /* Packages */ = { | |
| 61 | + isa = PBXGroup; | |
| 62 | + children = ( | |
| 63 | + 24D84DAB354D702D8575D587 /* MetrikaKit */, | |
| 64 | + ); | |
| 65 | + name = Packages; | |
| 66 | + sourceTree = "<group>"; | |
| 67 | + }; | |
| 68 | + A905F8EA05CC983967935417 /* Metrika */ = { | |
| 69 | + isa = PBXGroup; | |
| 70 | + children = ( | |
| 71 | + 0404FDEAF8A57414EFB3C649 /* Assets.xcassets */, | |
| 72 | + 6E94C5817E96C83B888FB579 /* Metrika.entitlements */, | |
| 73 | + 58DB3AB5F820BD1E6C82E1CB /* Sources */, | |
| 74 | + ); | |
| 75 | + path = Metrika; | |
| 76 | + sourceTree = "<group>"; | |
| 77 | + }; | |
| 78 | + DC657A037F59093C5529DD71 = { | |
| 79 | + isa = PBXGroup; | |
| 80 | + children = ( | |
| 81 | + A905F8EA05CC983967935417 /* Metrika */, | |
| 82 | + 63F77F28B4EF42E0407BEC4F /* Packages */, | |
| 83 | + 133558B8D49331BB5120F8F8 /* Products */, | |
| 84 | + ); | |
| 85 | + sourceTree = "<group>"; | |
| 86 | + }; | |
| 87 | +/* End PBXGroup section */ | |
| 88 | + | |
| 89 | +/* Begin PBXNativeTarget section */ | |
| 90 | + 5E03B33ABE9DE3162C326F87 /* Metrika */ = { | |
| 91 | + isa = PBXNativeTarget; | |
| 92 | + buildConfigurationList = A087F0DEACCF786E9B7CC9A5 /* Build configuration list for PBXNativeTarget "Metrika" */; | |
| 93 | + buildPhases = ( | |
| 94 | + C8918D5868AE76549F9247E4 /* Sources */, | |
| 95 | + D09C5D18E3FF44946887A6C9 /* Resources */, | |
| 96 | + 168283EE9449D9A4324BFE40 /* Frameworks */, | |
| 97 | + ); | |
| 98 | + buildRules = ( | |
| 99 | + ); | |
| 100 | + dependencies = ( | |
| 101 | + ); | |
| 102 | + name = Metrika; | |
| 103 | + packageProductDependencies = ( | |
| 104 | + 498C4115F45BADDF16191A57 /* MetrikaKit */, | |
| 105 | + ); | |
| 106 | + productName = Metrika; | |
| 107 | + productReference = 5A0213CC9FE05A339DEEFA28 /* Metrika.app */; | |
| 108 | + productType = "com.apple.product-type.application"; | |
| 109 | + }; | |
| 110 | +/* End PBXNativeTarget section */ | |
| 111 | + | |
| 112 | +/* Begin PBXProject section */ | |
| 113 | + 8571D0059EB0A97DE47F96F7 /* Project object */ = { | |
| 114 | + isa = PBXProject; | |
| 115 | + attributes = { | |
| 116 | + BuildIndependentTargetsInParallel = YES; | |
| 117 | + LastUpgradeCheck = 1430; | |
| 118 | + TargetAttributes = { | |
| 119 | + 5E03B33ABE9DE3162C326F87 = { | |
| 120 | + ProvisioningStyle = Automatic; | |
| 121 | + }; | |
| 122 | + }; | |
| 123 | + }; | |
| 124 | + buildConfigurationList = 02D661894CE6081B5C88E20C /* Build configuration list for PBXProject "Metrika" */; | |
| 125 | + developmentRegion = en; | |
| 126 | + hasScannedForEncodings = 0; | |
| 127 | + knownRegions = ( | |
| 128 | + Base, | |
| 129 | + en, | |
| 130 | + ); | |
| 131 | + mainGroup = DC657A037F59093C5529DD71; | |
| 132 | + minimizedProjectReferenceProxies = 1; | |
| 133 | + packageReferences = ( | |
| 134 | + D626AF14268CCEC6F4756ABC /* XCLocalSwiftPackageReference "MetrikaKit" */, | |
| 135 | + ); | |
| 136 | + preferredProjectObjectVersion = 77; | |
| 137 | + productRefGroup = 133558B8D49331BB5120F8F8 /* Products */; | |
| 138 | + projectDirPath = ""; | |
| 139 | + projectRoot = ""; | |
| 140 | + targets = ( | |
| 141 | + 5E03B33ABE9DE3162C326F87 /* Metrika */, | |
| 142 | + ); | |
| 143 | + }; | |
| 144 | +/* End PBXProject section */ | |
| 145 | + | |
| 146 | +/* Begin PBXResourcesBuildPhase section */ | |
| 147 | + D09C5D18E3FF44946887A6C9 /* Resources */ = { | |
| 148 | + isa = PBXResourcesBuildPhase; | |
| 149 | + buildActionMask = 2147483647; | |
| 150 | + files = ( | |
| 151 | + 3084AE2A328ED6746D01F46E /* Assets.xcassets in Resources */, | |
| 152 | + ); | |
| 153 | + runOnlyForDeploymentPostprocessing = 0; | |
| 154 | + }; | |
| 155 | +/* End PBXResourcesBuildPhase section */ | |
| 156 | + | |
| 157 | +/* Begin PBXSourcesBuildPhase section */ | |
| 158 | + C8918D5868AE76549F9247E4 /* Sources */ = { | |
| 159 | + isa = PBXSourcesBuildPhase; | |
| 160 | + buildActionMask = 2147483647; | |
| 161 | + files = ( | |
| 162 | + 8B0CDB213D64E3EE87593808 /* ConsoleView.swift in Sources */, | |
| 163 | + 9BE65709781719FAE0CA740A /* MetrikaApp.swift in Sources */, | |
| 164 | + DDCC2DC487AF9FAC7D6B8E3E /* PlotView.swift in Sources */, | |
| 165 | + 7E936A23CCC90823D6B08BCC /* SessionModel.swift in Sources */, | |
| 166 | + ); | |
| 167 | + runOnlyForDeploymentPostprocessing = 0; | |
| 168 | + }; | |
| 169 | +/* End PBXSourcesBuildPhase section */ | |
| 170 | + | |
| 171 | +/* Begin XCBuildConfiguration section */ | |
| 172 | + 3CB58B068179C9DB2CD1CAC1 /* Release */ = { | |
| 173 | + isa = XCBuildConfiguration; | |
| 174 | + buildSettings = { | |
| 175 | + ALWAYS_SEARCH_USER_PATHS = NO; | |
| 176 | + CLANG_ANALYZER_NONNULL = YES; | |
| 177 | + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | |
| 178 | + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; | |
| 179 | + CLANG_CXX_LIBRARY = "libc++"; | |
| 180 | + CLANG_ENABLE_MODULES = YES; | |
| 181 | + CLANG_ENABLE_OBJC_ARC = YES; | |
| 182 | + CLANG_ENABLE_OBJC_WEAK = YES; | |
| 183 | + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; | |
| 184 | + CLANG_WARN_BOOL_CONVERSION = YES; | |
| 185 | + CLANG_WARN_COMMA = YES; | |
| 186 | + CLANG_WARN_CONSTANT_CONVERSION = YES; | |
| 187 | + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; | |
| 188 | + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; | |
| 189 | + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | |
| 190 | + CLANG_WARN_EMPTY_BODY = YES; | |
| 191 | + CLANG_WARN_ENUM_CONVERSION = YES; | |
| 192 | + CLANG_WARN_INFINITE_RECURSION = YES; | |
| 193 | + CLANG_WARN_INT_CONVERSION = YES; | |
| 194 | + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; | |
| 195 | + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; | |
| 196 | + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; | |
| 197 | + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; | |
| 198 | + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | |
| 199 | + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; | |
| 200 | + CLANG_WARN_STRICT_PROTOTYPES = YES; | |
| 201 | + CLANG_WARN_SUSPICIOUS_MOVE = YES; | |
| 202 | + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | |
| 203 | + CLANG_WARN_UNREACHABLE_CODE = YES; | |
| 204 | + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; | |
| 205 | + COPY_PHASE_STRIP = NO; | |
| 206 | + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; | |
| 207 | + ENABLE_NS_ASSERTIONS = NO; | |
| 208 | + ENABLE_STRICT_OBJC_MSGSEND = YES; | |
| 209 | + GCC_C_LANGUAGE_STANDARD = gnu11; | |
| 210 | + GCC_NO_COMMON_BLOCKS = YES; | |
| 211 | + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; | |
| 212 | + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; | |
| 213 | + GCC_WARN_UNDECLARED_SELECTOR = YES; | |
| 214 | + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | |
| 215 | + GCC_WARN_UNUSED_FUNCTION = YES; | |
| 216 | + GCC_WARN_UNUSED_VARIABLE = YES; | |
| 217 | + MACOSX_DEPLOYMENT_TARGET = 14.0; | |
| 218 | + MTL_ENABLE_DEBUG_INFO = NO; | |
| 219 | + MTL_FAST_MATH = YES; | |
| 220 | + PRODUCT_NAME = "$(TARGET_NAME)"; | |
| 221 | + SDKROOT = macosx; | |
| 222 | + SWIFT_COMPILATION_MODE = wholemodule; | |
| 223 | + SWIFT_OPTIMIZATION_LEVEL = "-O"; | |
| 224 | + SWIFT_VERSION = 5.0; | |
| 225 | + }; | |
| 226 | + name = Release; | |
| 227 | + }; | |
| 228 | + 4D5E4B6EED8197B03EAD7E51 /* Debug */ = { | |
| 229 | + isa = XCBuildConfiguration; | |
| 230 | + buildSettings = { | |
| 231 | + ARCHS = arm64; | |
| 232 | + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | |
| 233 | + CODE_SIGN_ENTITLEMENTS = Metrika/Metrika.entitlements; | |
| 234 | + CODE_SIGN_STYLE = Automatic; | |
| 235 | + COMBINE_HIDPI_IMAGES = YES; | |
| 236 | + CURRENT_PROJECT_VERSION = 1; | |
| 237 | + ENABLE_APP_SANDBOX = YES; | |
| 238 | + ENABLE_HARDENED_RUNTIME = YES; | |
| 239 | + GENERATE_INFOPLIST_FILE = YES; | |
| 240 | + INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Simon-Pierre Boucher. All rights reserved."; | |
| 241 | + INFOPLIST_KEY_NSPrincipalClass = NSApplication; | |
| 242 | + LD_RUNPATH_SEARCH_PATHS = ( | |
| 243 | + "$(inherited)", | |
| 244 | + "@executable_path/../Frameworks", | |
| 245 | + ); | |
| 246 | + MARKETING_VERSION = 0.1.0; | |
| 247 | + ONLY_ACTIVE_ARCH = YES; | |
| 248 | + PRODUCT_BUNDLE_IDENTIFIER = ai.spboucher.metrika; | |
| 249 | + SDKROOT = macosx; | |
| 250 | + SWIFT_STRICT_CONCURRENCY = complete; | |
| 251 | + SWIFT_VERSION = 6.0; | |
| 252 | + }; | |
| 253 | + name = Debug; | |
| 254 | + }; | |
| 255 | + C241F8724816C2E6FEFA98F2 /* Release */ = { | |
| 256 | + isa = XCBuildConfiguration; | |
| 257 | + buildSettings = { | |
| 258 | + ARCHS = arm64; | |
| 259 | + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | |
| 260 | + CODE_SIGN_ENTITLEMENTS = Metrika/Metrika.entitlements; | |
| 261 | + CODE_SIGN_STYLE = Automatic; | |
| 262 | + COMBINE_HIDPI_IMAGES = YES; | |
| 263 | + CURRENT_PROJECT_VERSION = 1; | |
| 264 | + ENABLE_APP_SANDBOX = YES; | |
| 265 | + ENABLE_HARDENED_RUNTIME = YES; | |
| 266 | + GENERATE_INFOPLIST_FILE = YES; | |
| 267 | + INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Simon-Pierre Boucher. All rights reserved."; | |
| 268 | + INFOPLIST_KEY_NSPrincipalClass = NSApplication; | |
| 269 | + LD_RUNPATH_SEARCH_PATHS = ( | |
| 270 | + "$(inherited)", | |
| 271 | + "@executable_path/../Frameworks", | |
| 272 | + ); | |
| 273 | + MARKETING_VERSION = 0.1.0; | |
| 274 | + ONLY_ACTIVE_ARCH = YES; | |
| 275 | + PRODUCT_BUNDLE_IDENTIFIER = ai.spboucher.metrika; | |
| 276 | + SDKROOT = macosx; | |
| 277 | + SWIFT_STRICT_CONCURRENCY = complete; | |
| 278 | + SWIFT_VERSION = 6.0; | |
| 279 | + }; | |
| 280 | + name = Release; | |
| 281 | + }; | |
| 282 | + DC41815671596F5C0AC0A0A6 /* Debug */ = { | |
| 283 | + isa = XCBuildConfiguration; | |
| 284 | + buildSettings = { | |
| 285 | + ALWAYS_SEARCH_USER_PATHS = NO; | |
| 286 | + CLANG_ANALYZER_NONNULL = YES; | |
| 287 | + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | |
| 288 | + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; | |
| 289 | + CLANG_CXX_LIBRARY = "libc++"; | |
| 290 | + CLANG_ENABLE_MODULES = YES; | |
| 291 | + CLANG_ENABLE_OBJC_ARC = YES; | |
| 292 | + CLANG_ENABLE_OBJC_WEAK = YES; | |
| 293 | + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; | |
| 294 | + CLANG_WARN_BOOL_CONVERSION = YES; | |
| 295 | + CLANG_WARN_COMMA = YES; | |
| 296 | + CLANG_WARN_CONSTANT_CONVERSION = YES; | |
| 297 | + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; | |
| 298 | + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; | |
| 299 | + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | |
| 300 | + CLANG_WARN_EMPTY_BODY = YES; | |
| 301 | + CLANG_WARN_ENUM_CONVERSION = YES; | |
| 302 | + CLANG_WARN_INFINITE_RECURSION = YES; | |
| 303 | + CLANG_WARN_INT_CONVERSION = YES; | |
| 304 | + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; | |
| 305 | + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; | |
| 306 | + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; | |
| 307 | + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; | |
| 308 | + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | |
| 309 | + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; | |
| 310 | + CLANG_WARN_STRICT_PROTOTYPES = YES; | |
| 311 | + CLANG_WARN_SUSPICIOUS_MOVE = YES; | |
| 312 | + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | |
| 313 | + CLANG_WARN_UNREACHABLE_CODE = YES; | |
| 314 | + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; | |
| 315 | + COPY_PHASE_STRIP = NO; | |
| 316 | + DEBUG_INFORMATION_FORMAT = dwarf; | |
| 317 | + ENABLE_STRICT_OBJC_MSGSEND = YES; | |
| 318 | + ENABLE_TESTABILITY = YES; | |
| 319 | + GCC_C_LANGUAGE_STANDARD = gnu11; | |
| 320 | + GCC_DYNAMIC_NO_PIC = NO; | |
| 321 | + GCC_NO_COMMON_BLOCKS = YES; | |
| 322 | + GCC_OPTIMIZATION_LEVEL = 0; | |
| 323 | + GCC_PREPROCESSOR_DEFINITIONS = ( | |
| 324 | + "$(inherited)", | |
| 325 | + "DEBUG=1", | |
| 326 | + ); | |
| 327 | + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; | |
| 328 | + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; | |
| 329 | + GCC_WARN_UNDECLARED_SELECTOR = YES; | |
| 330 | + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | |
| 331 | + GCC_WARN_UNUSED_FUNCTION = YES; | |
| 332 | + GCC_WARN_UNUSED_VARIABLE = YES; | |
| 333 | + MACOSX_DEPLOYMENT_TARGET = 14.0; | |
| 334 | + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; | |
| 335 | + MTL_FAST_MATH = YES; | |
| 336 | + ONLY_ACTIVE_ARCH = YES; | |
| 337 | + PRODUCT_NAME = "$(TARGET_NAME)"; | |
| 338 | + SDKROOT = macosx; | |
| 339 | + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; | |
| 340 | + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | |
| 341 | + SWIFT_VERSION = 5.0; | |
| 342 | + }; | |
| 343 | + name = Debug; | |
| 344 | + }; | |
| 345 | +/* End XCBuildConfiguration section */ | |
| 346 | + | |
| 347 | +/* Begin XCConfigurationList section */ | |
| 348 | + 02D661894CE6081B5C88E20C /* Build configuration list for PBXProject "Metrika" */ = { | |
| 349 | + isa = XCConfigurationList; | |
| 350 | + buildConfigurations = ( | |
| 351 | + DC41815671596F5C0AC0A0A6 /* Debug */, | |
| 352 | + 3CB58B068179C9DB2CD1CAC1 /* Release */, | |
| 353 | + ); | |
| 354 | + defaultConfigurationIsVisible = 0; | |
| 355 | + defaultConfigurationName = Debug; | |
| 356 | + }; | |
| 357 | + A087F0DEACCF786E9B7CC9A5 /* Build configuration list for PBXNativeTarget "Metrika" */ = { | |
| 358 | + isa = XCConfigurationList; | |
| 359 | + buildConfigurations = ( | |
| 360 | + 4D5E4B6EED8197B03EAD7E51 /* Debug */, | |
| 361 | + C241F8724816C2E6FEFA98F2 /* Release */, | |
| 362 | + ); | |
| 363 | + defaultConfigurationIsVisible = 0; | |
| 364 | + defaultConfigurationName = Debug; | |
| 365 | + }; | |
| 366 | +/* End XCConfigurationList section */ | |
| 367 | + | |
| 368 | +/* Begin XCLocalSwiftPackageReference section */ | |
| 369 | + D626AF14268CCEC6F4756ABC /* XCLocalSwiftPackageReference "MetrikaKit" */ = { | |
| 370 | + isa = XCLocalSwiftPackageReference; | |
| 371 | + relativePath = MetrikaKit; | |
| 372 | + }; | |
| 373 | +/* End XCLocalSwiftPackageReference section */ | |
| 374 | + | |
| 375 | +/* Begin XCSwiftPackageProductDependency section */ | |
| 376 | + 498C4115F45BADDF16191A57 /* MetrikaKit */ = { | |
| 377 | + isa = XCSwiftPackageProductDependency; | |
| 378 | + productName = MetrikaKit; | |
| 379 | + }; | |
| 380 | +/* End XCSwiftPackageProductDependency section */ | |
| 381 | + }; | |
| 382 | + rootObject = 8571D0059EB0A97DE47F96F7 /* Project object */; | |
| 383 | +} | |
added
Metrika.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<Workspace | |
| 3 | + version = "1.0"> | |
| 4 | + <FileRef | |
| 5 | + location = "self:"> | |
| 6 | + </FileRef> | |
| 7 | +</Workspace> | |
added
Metrika/Assets.xcassets/AppIcon.appiconset/Contents.json
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +{ | |
| 2 | + "images": [ | |
| 3 | + { "filename": "icon_16.png", "idiom": "mac", "scale": "1x", "size": "16x16" }, | |
| 4 | + { "filename": "icon_16@2x.png", "idiom": "mac", "scale": "2x", "size": "16x16" }, | |
| 5 | + { "filename": "icon_32.png", "idiom": "mac", "scale": "1x", "size": "32x32" }, | |
| 6 | + { "filename": "icon_32@2x.png", "idiom": "mac", "scale": "2x", "size": "32x32" }, | |
| 7 | + { "filename": "icon_128.png", "idiom": "mac", "scale": "1x", "size": "128x128" }, | |
| 8 | + { "filename": "icon_128@2x.png", "idiom": "mac", "scale": "2x", "size": "128x128" }, | |
| 9 | + { "filename": "icon_256.png", "idiom": "mac", "scale": "1x", "size": "256x256" }, | |
| 10 | + { "filename": "icon_256@2x.png", "idiom": "mac", "scale": "2x", "size": "256x256" }, | |
| 11 | + { "filename": "icon_512.png", "idiom": "mac", "scale": "1x", "size": "512x512" }, | |
| 12 | + { "filename": "icon_512@2x.png", "idiom": "mac", "scale": "2x", "size": "512x512" } | |
| 13 | + ], | |
| 14 | + "info": { "author": "xcode", "version": 1 } | |
| 15 | +} | |
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_128.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_16.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_256.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_32.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_512.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png
+0 −0
Binary file not shown.
added
Metrika/Assets.xcassets/Contents.json
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +{ "info": { "author": "xcode", "version": 1 } } | |
added
Metrika/Metrika.entitlements
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" | |
| 3 | + "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 4 | +<plist version="1.0"> | |
| 5 | +<dict> | |
| 6 | + <key>com.apple.security.app-sandbox</key> | |
| 7 | + <true/> | |
| 8 | + <key>com.apple.security.files.user-selected.read-write</key> | |
| 9 | + <true/> | |
| 10 | + <key>com.apple.security.files.bookmarks.app-scope</key> | |
| 11 | + <true/> | |
| 12 | +</dict> | |
| 13 | +</plist> | |
added
Metrika/Sources/ConsoleView.swift
+120 −0
@@ -0,0 +1,120 @@ | ||
| 1 | +// | |
| 2 | +// ConsoleView.swift | |
| 3 | +// Metrika | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Contact: contact@spboucher.ai | |
| 7 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 8 | +// | |
| 9 | + | |
| 10 | +import SwiftUI | |
| 11 | + | |
| 12 | +/// Command console: scrollback of executed commands with their rendered | |
| 13 | +/// output, plus an input line with ↑/↓ history recall (CLAUDE.md §7). | |
| 14 | +struct ConsoleView: View { | |
| 15 | + @Environment(SessionModel.self) private var model | |
| 16 | + @State private var input = "" | |
| 17 | + @State private var historyCursor: Int? | |
| 18 | + @FocusState private var inputFocused: Bool | |
| 19 | + | |
| 20 | + var body: some View { | |
| 21 | + VStack(spacing: 0) { | |
| 22 | + ScrollViewReader { proxy in | |
| 23 | + ScrollView { | |
| 24 | + LazyVStack(alignment: .leading, spacing: 10) { | |
| 25 | + ForEach(model.entries) { entry in | |
| 26 | + VStack(alignment: .leading, spacing: 2) { | |
| 27 | + if !entry.command.isEmpty { | |
| 28 | + Text(". \(entry.command)") | |
| 29 | + .foregroundStyle(.secondary) | |
| 30 | + } | |
| 31 | + if !entry.output.isEmpty { | |
| 32 | + Text(entry.output) | |
| 33 | + .foregroundStyle( | |
| 34 | + entry.isError ? Color.red : Color.primary | |
| 35 | + ) | |
| 36 | + .textSelection(.enabled) | |
| 37 | + } | |
| 38 | + } | |
| 39 | + .id(entry.id) | |
| 40 | + } | |
| 41 | + } | |
| 42 | + .padding(12) | |
| 43 | + .frame(maxWidth: .infinity, alignment: .leading) | |
| 44 | + } | |
| 45 | + .onChange(of: model.entries) { | |
| 46 | + if let last = model.entries.last { | |
| 47 | + proxy.scrollTo(last.id, anchor: .bottom) | |
| 48 | + } | |
| 49 | + } | |
| 50 | + } | |
| 51 | + | |
| 52 | + Divider() | |
| 53 | + | |
| 54 | + HStack(spacing: 8) { | |
| 55 | + Text(".") | |
| 56 | + .foregroundStyle(.secondary) | |
| 57 | + TextField("Type a command — e.g. use sales.parquet", text: $input) | |
| 58 | + .textFieldStyle(.plain) | |
| 59 | + .focused($inputFocused) | |
| 60 | + .onSubmit(submit) | |
| 61 | + .onKeyPress(.upArrow) { recallHistory(step: -1) } | |
| 62 | + .onKeyPress(.downArrow) { recallHistory(step: 1) } | |
| 63 | + if model.isRunning { | |
| 64 | + ProgressView() | |
| 65 | + .controlSize(.small) | |
| 66 | + } | |
| 67 | + } | |
| 68 | + .padding(10) | |
| 69 | + } | |
| 70 | + .fontDesign(.monospaced) | |
| 71 | + .font(.system(size: 12)) | |
| 72 | + .onAppear { inputFocused = true } | |
| 73 | + } | |
| 74 | + | |
| 75 | + private func submit() { | |
| 76 | + let command = input | |
| 77 | + input = "" | |
| 78 | + historyCursor = nil | |
| 79 | + Task { await model.run(command) } | |
| 80 | + } | |
| 81 | + | |
| 82 | + private func recallHistory(step: Int) -> KeyPress.Result { | |
| 83 | + let history = model.history | |
| 84 | + guard !history.isEmpty else { return .ignored } | |
| 85 | + var cursor = historyCursor ?? history.count | |
| 86 | + cursor = min(max(cursor + step, 0), history.count) | |
| 87 | + historyCursor = cursor | |
| 88 | + input = cursor < history.count ? history[cursor] : "" | |
| 89 | + return .handled | |
| 90 | + } | |
| 91 | +} | |
| 92 | + | |
| 93 | +/// Sidebar: variables of the working dataset with type and missing count | |
| 94 | +/// (CLAUDE.md §7, pane 5). | |
| 95 | +struct VariablesSidebar: View { | |
| 96 | + @Environment(SessionModel.self) private var model | |
| 97 | + | |
| 98 | + var body: some View { | |
| 99 | + List { | |
| 100 | + Section("Variables (\(model.observationCount) obs)") { | |
| 101 | + ForEach(model.variables) { variable in | |
| 102 | + HStack { | |
| 103 | + Text(variable.name) | |
| 104 | + .fontDesign(.monospaced) | |
| 105 | + Spacer() | |
| 106 | + if variable.missingCount > 0 { | |
| 107 | + Text("\(variable.missingCount) mi") | |
| 108 | + .foregroundStyle(.orange) | |
| 109 | + .font(.caption) | |
| 110 | + } | |
| 111 | + Text(variable.type) | |
| 112 | + .foregroundStyle(.secondary) | |
| 113 | + .font(.caption) | |
| 114 | + } | |
| 115 | + } | |
| 116 | + } | |
| 117 | + } | |
| 118 | + .listStyle(.sidebar) | |
| 119 | + } | |
| 120 | +} | |
added
Metrika/Sources/MetrikaApp.swift
+44 −0
@@ -0,0 +1,44 @@ | ||
| 1 | +// | |
| 2 | +// MetrikaApp.swift | |
| 3 | +// Metrika | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Contact: contact@spboucher.ai | |
| 7 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 8 | +// | |
| 9 | + | |
| 10 | +import SwiftUI | |
| 11 | + | |
| 12 | +@main | |
| 13 | +struct MetrikaApp: App { | |
| 14 | + @State private var model = SessionModel() | |
| 15 | + | |
| 16 | + var body: some Scene { | |
| 17 | + WindowGroup { | |
| 18 | + ContentView() | |
| 19 | + .environment(model) | |
| 20 | + .frame(minWidth: 900, minHeight: 560) | |
| 21 | + } | |
| 22 | + .windowStyle(.automatic) | |
| 23 | + } | |
| 24 | +} | |
| 25 | + | |
| 26 | +struct ContentView: View { | |
| 27 | + @Environment(SessionModel.self) private var model | |
| 28 | + | |
| 29 | + var body: some View { | |
| 30 | + NavigationSplitView { | |
| 31 | + VariablesSidebar() | |
| 32 | + } detail: { | |
| 33 | + HSplitView { | |
| 34 | + ConsoleView() | |
| 35 | + .frame(minWidth: 480) | |
| 36 | + if model.lastPlot != nil { | |
| 37 | + PlotView() | |
| 38 | + .frame(minWidth: 300) | |
| 39 | + } | |
| 40 | + } | |
| 41 | + } | |
| 42 | + .navigationTitle("Metrika") | |
| 43 | + } | |
| 44 | +} | |
added
Metrika/Sources/PlotView.swift
+58 −0
@@ -0,0 +1,58 @@ | ||
| 1 | +// | |
| 2 | +// PlotView.swift | |
| 3 | +// Metrika | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Contact: contact@spboucher.ai | |
| 7 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 8 | +// | |
| 9 | + | |
| 10 | +import Charts | |
| 11 | +import SwiftUI | |
| 12 | +import ZQGraphics | |
| 13 | + | |
| 14 | +/// Swift Charts rendering of the session's latest plot spec. The custom | |
| 15 | +/// Metal renderer takes over past ~1M points (v0.3, CLAUDE.md §7). | |
| 16 | +struct PlotView: View { | |
| 17 | + @Environment(SessionModel.self) private var model | |
| 18 | + | |
| 19 | + var body: some View { | |
| 20 | + if let plot = model.lastPlot { | |
| 21 | + Chart { | |
| 22 | + ForEach(Array(plot.series.enumerated()), id: \.offset) { _, series in | |
| 23 | + ForEach(series.x.indices, id: \.self) { index in | |
| 24 | + switch plot.kind { | |
| 25 | + case .scatter: | |
| 26 | + PointMark( | |
| 27 | + x: .value(plot.xLabel, series.x[index]), | |
| 28 | + y: .value(plot.yLabel, series.y[index]) | |
| 29 | + ) | |
| 30 | + .symbolSize(20) | |
| 31 | + .foregroundStyle(by: .value("Series", series.label)) | |
| 32 | + case .line, .kdensity: | |
| 33 | + LineMark( | |
| 34 | + x: .value(plot.xLabel, series.x[index]), | |
| 35 | + y: .value(plot.yLabel, series.y[index]) | |
| 36 | + ) | |
| 37 | + .foregroundStyle(by: .value("Series", series.label)) | |
| 38 | + case .histogram: | |
| 39 | + BarMark( | |
| 40 | + x: .value(plot.xLabel, series.x[index]), | |
| 41 | + y: .value(plot.yLabel, series.y[index]) | |
| 42 | + ) | |
| 43 | + .foregroundStyle(by: .value("Series", series.label)) | |
| 44 | + } | |
| 45 | + } | |
| 46 | + } | |
| 47 | + } | |
| 48 | + .chartXAxisLabel(plot.xLabel) | |
| 49 | + .chartYAxisLabel(plot.yLabel) | |
| 50 | + .padding(12) | |
| 51 | + } else { | |
| 52 | + ContentUnavailableView( | |
| 53 | + "No plot", systemImage: "chart.xyaxis.line", | |
| 54 | + description: Text("Run a graph command to see it here.") | |
| 55 | + ) | |
| 56 | + } | |
| 57 | + } | |
| 58 | +} | |
added
Metrika/Sources/SessionModel.swift
+84 −0
@@ -0,0 +1,84 @@ | ||
| 1 | +// | |
| 2 | +// SessionModel.swift | |
| 3 | +// Metrika | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Contact: contact@spboucher.ai | |
| 7 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 8 | +// | |
| 9 | + | |
| 10 | +import Observation | |
| 11 | +import SwiftUI | |
| 12 | +import ZQData | |
| 13 | +import ZQEngine | |
| 14 | +import ZQGraphics | |
| 15 | + | |
| 16 | +/// Observable session state for the UI. Every command — typed in the | |
| 17 | +/// console or replayed from a do-file — goes through the one `ZQSession` | |
| 18 | +/// execution path (CLAUDE.md §7). | |
| 19 | +@Observable @MainActor | |
| 20 | +final class SessionModel { | |
| 21 | + struct ConsoleEntry: Identifiable, Equatable { | |
| 22 | + let id = UUID() | |
| 23 | + let command: String | |
| 24 | + let output: String | |
| 25 | + let isError: Bool | |
| 26 | + } | |
| 27 | + | |
| 28 | + struct VariableInfo: Identifiable, Equatable { | |
| 29 | + var id: String { name } | |
| 30 | + let name: String | |
| 31 | + let type: String | |
| 32 | + let missingCount: Int | |
| 33 | + } | |
| 34 | + | |
| 35 | + private(set) var entries: [ConsoleEntry] = [] | |
| 36 | + private(set) var variables: [VariableInfo] = [] | |
| 37 | + private(set) var observationCount = 0 | |
| 38 | + private(set) var lastPlot: ZQPlotSpec? | |
| 39 | + private(set) var isRunning = false | |
| 40 | + private(set) var history: [String] = [] | |
| 41 | + | |
| 42 | + private var session: ZQSession? | |
| 43 | + | |
| 44 | + init() { | |
| 45 | + do { | |
| 46 | + session = try ZQSession() | |
| 47 | + } catch { | |
| 48 | + entries.append(ConsoleEntry( | |
| 49 | + command: "", | |
| 50 | + output: "failed to start engine: \(error)", | |
| 51 | + isError: true | |
| 52 | + )) | |
| 53 | + } | |
| 54 | + } | |
| 55 | + | |
| 56 | + func run(_ command: String) async { | |
| 57 | + let line = command.trimmingCharacters(in: .whitespaces) | |
| 58 | + guard !line.isEmpty, let session else { return } | |
| 59 | + history.append(line) | |
| 60 | + isRunning = true | |
| 61 | + defer { isRunning = false } | |
| 62 | + do { | |
| 63 | + let result = try await session.execute(line) | |
| 64 | + entries.append(ConsoleEntry(command: line, output: result.text, isError: false)) | |
| 65 | + } catch { | |
| 66 | + entries.append(ConsoleEntry(command: line, output: "\(error)", isError: true)) | |
| 67 | + } | |
| 68 | + await refreshDatasetState() | |
| 69 | + } | |
| 70 | + | |
| 71 | + private func refreshDatasetState() async { | |
| 72 | + guard let session else { return } | |
| 73 | + let frame = await session.frame | |
| 74 | + observationCount = frame.rowCount | |
| 75 | + variables = frame.columns.map { column in | |
| 76 | + VariableInfo( | |
| 77 | + name: column.name, | |
| 78 | + type: column.data.isNumeric ? "float64" : "string", | |
| 79 | + missingCount: column.data.missingCount | |
| 80 | + ) | |
| 81 | + } | |
| 82 | + lastPlot = await session.lastPlot | |
| 83 | + } | |
| 84 | +} | |
added
MetrikaKit/Package.resolved
+33 −0
@@ -0,0 +1,33 @@ | ||
| 1 | +{ | |
| 2 | + "originHash" : "e4ae512a67bec7ae66f2abd23f1e87240c39d28811734cdfafc7a91e4323f6db", | |
| 3 | + "pins" : [ | |
| 4 | + { | |
| 5 | + "identity" : "duckdb-swift", | |
| 6 | + "kind" : "remoteSourceControl", | |
| 7 | + "location" : "https://github.com/duckdb/duckdb-swift", | |
| 8 | + "state" : { | |
| 9 | + "revision" : "9583f44fedc20a8e0b6151d2e43f6d850016712f", | |
| 10 | + "version" : "1.1.3" | |
| 11 | + } | |
| 12 | + }, | |
| 13 | + { | |
| 14 | + "identity" : "swift-argument-parser", | |
| 15 | + "kind" : "remoteSourceControl", | |
| 16 | + "location" : "https://github.com/apple/swift-argument-parser", | |
| 17 | + "state" : { | |
| 18 | + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", | |
| 19 | + "version" : "1.8.2" | |
| 20 | + } | |
| 21 | + }, | |
| 22 | + { | |
| 23 | + "identity" : "swift-collections", | |
| 24 | + "kind" : "remoteSourceControl", | |
| 25 | + "location" : "https://github.com/apple/swift-collections", | |
| 26 | + "state" : { | |
| 27 | + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", | |
| 28 | + "version" : "1.6.0" | |
| 29 | + } | |
| 30 | + } | |
| 31 | + ], | |
| 32 | + "version" : 3 | |
| 33 | +} | |
added
MetrikaKit/Package.swift
+96 −0
@@ -0,0 +1,96 @@ | ||
| 1 | +// swift-tools-version: 6.0 | |
| 2 | +// | |
| 3 | +// Package.swift | |
| 4 | +// Metrika | |
| 5 | +// | |
| 6 | +// Author: Simon-Pierre Boucher | |
| 7 | +// Contact: contact@spboucher.ai | |
| 8 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 9 | +// | |
| 10 | + | |
| 11 | +import PackageDescription | |
| 12 | + | |
| 13 | +let strictConcurrency: [SwiftSetting] = [ | |
| 14 | + .swiftLanguageMode(.v6) | |
| 15 | +] | |
| 16 | + | |
| 17 | +let package = Package( | |
| 18 | + name: "MetrikaKit", | |
| 19 | + platforms: [.macOS(.v14)], | |
| 20 | + products: [ | |
| 21 | + .library( | |
| 22 | + name: "MetrikaKit", | |
| 23 | + targets: [ | |
| 24 | + "ZQParser", "ZQPlanner", "ZQEngine", "ZQData", | |
| 25 | + "ZQStats", "ZQGPU", "ZQGraphics", "ZQPlugins", | |
| 26 | + ] | |
| 27 | + ), | |
| 28 | + .executable(name: "metrika-cli", targets: ["MetrikaCLI"]), | |
| 29 | + ], | |
| 30 | + dependencies: [ | |
| 31 | + .package(url: "https://github.com/duckdb/duckdb-swift", from: "1.0.0"), | |
| 32 | + .package(url: "https://github.com/apple/swift-collections", from: "1.1.0"), | |
| 33 | + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), | |
| 34 | + ], | |
| 35 | + targets: [ | |
| 36 | + .target( | |
| 37 | + name: "ZQParser", | |
| 38 | + swiftSettings: strictConcurrency | |
| 39 | + ), | |
| 40 | + .target( | |
| 41 | + name: "ZQData", | |
| 42 | + dependencies: [ | |
| 43 | + .product(name: "DuckDB", package: "duckdb-swift"), | |
| 44 | + .product(name: "Collections", package: "swift-collections"), | |
| 45 | + ], | |
| 46 | + swiftSettings: strictConcurrency | |
| 47 | + ), | |
| 48 | + .target( | |
| 49 | + name: "ZQStats", | |
| 50 | + dependencies: ["ZQData"], | |
| 51 | + swiftSettings: strictConcurrency | |
| 52 | + ), | |
| 53 | + .target( | |
| 54 | + name: "ZQGPU", | |
| 55 | + dependencies: ["ZQData"], | |
| 56 | + swiftSettings: strictConcurrency | |
| 57 | + ), | |
| 58 | + .target( | |
| 59 | + name: "ZQPlanner", | |
| 60 | + dependencies: ["ZQParser", "ZQData"], | |
| 61 | + swiftSettings: strictConcurrency | |
| 62 | + ), | |
| 63 | + .target( | |
| 64 | + name: "ZQGraphics", | |
| 65 | + dependencies: ["ZQData"], | |
| 66 | + swiftSettings: strictConcurrency | |
| 67 | + ), | |
| 68 | + .target( | |
| 69 | + name: "ZQPlugins", | |
| 70 | + dependencies: ["ZQParser", "ZQData"], | |
| 71 | + swiftSettings: strictConcurrency | |
| 72 | + ), | |
| 73 | + .target( | |
| 74 | + name: "ZQEngine", | |
| 75 | + dependencies: [ | |
| 76 | + "ZQParser", "ZQPlanner", "ZQData", "ZQStats", | |
| 77 | + "ZQGPU", "ZQGraphics", "ZQPlugins", | |
| 78 | + ], | |
| 79 | + swiftSettings: strictConcurrency | |
| 80 | + ), | |
| 81 | + .executableTarget( | |
| 82 | + name: "MetrikaCLI", | |
| 83 | + dependencies: [ | |
| 84 | + "ZQEngine", | |
| 85 | + .product(name: "ArgumentParser", package: "swift-argument-parser"), | |
| 86 | + ], | |
| 87 | + swiftSettings: strictConcurrency | |
| 88 | + ), | |
| 89 | + .testTarget( | |
| 90 | + name: "MetrikaKitTests", | |
| 91 | + dependencies: ["ZQParser", "ZQPlanner", "ZQEngine", "ZQData", "ZQStats", "ZQGPU"], | |
| 92 | + resources: [.copy("Fixtures")], | |
| 93 | + swiftSettings: strictConcurrency | |
| 94 | + ), | |
| 95 | + ] | |
| 96 | +) | |
added
MetrikaKit/Sources/MetrikaCLI/MetrikaCLI.swift
+65 −0
@@ -0,0 +1,65 @@ | ||
| 1 | +// | |
| 2 | +// MetrikaCLI.swift | |
| 3 | +// Metrika | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Contact: contact@spboucher.ai | |
| 7 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 8 | +// | |
| 9 | + | |
| 10 | +import ArgumentParser | |
| 11 | +import Foundation | |
| 12 | +import ZQEngine | |
| 13 | + | |
| 14 | +/// Companion command-line tool: runs ZQL commands or `.zyq` do-files | |
| 15 | +/// against a headless session. Interactive REPL when no input is given. | |
| 16 | +@main | |
| 17 | +struct MetrikaCLI: AsyncParsableCommand { | |
| 18 | + static let configuration = CommandConfiguration( | |
| 19 | + commandName: "metrika-cli", | |
| 20 | + abstract: "Metrika statistical engine — command-line interface." | |
| 21 | + ) | |
| 22 | + | |
| 23 | + @Option( | |
| 24 | + name: [.short, .customLong("execute")], | |
| 25 | + help: "Execute a ZQL command (repeatable, runs in order)." | |
| 26 | + ) | |
| 27 | + var execute: [String] = [] | |
| 28 | + | |
| 29 | + @Argument(help: "A .zyq do-file to run.") | |
| 30 | + var doFile: String? | |
| 31 | + | |
| 32 | + mutating func run() async throws { | |
| 33 | + let session = try ZQSession() | |
| 34 | + | |
| 35 | + if let doFile { | |
| 36 | + let url = URL(fileURLWithPath: (doFile as NSString).expandingTildeInPath) | |
| 37 | + let text = try String(contentsOf: url, encoding: .utf8) | |
| 38 | + let result = try await session.executeScript(text) | |
| 39 | + print(result.text) | |
| 40 | + return | |
| 41 | + } | |
| 42 | + | |
| 43 | + if !execute.isEmpty { | |
| 44 | + for command in execute { | |
| 45 | + let result = try await session.execute(command) | |
| 46 | + if !result.text.isEmpty { print(result.text) } | |
| 47 | + } | |
| 48 | + return | |
| 49 | + } | |
| 50 | + | |
| 51 | + // REPL | |
| 52 | + print("Metrika — type ZQL commands, 'exit' to quit.") | |
| 53 | + while true { | |
| 54 | + print(". ", terminator: "") | |
| 55 | + guard let line = readLine() else { break } | |
| 56 | + if line == "exit" || line == "quit" { break } | |
| 57 | + do { | |
| 58 | + let result = try await session.execute(line) | |
| 59 | + if !result.text.isEmpty { print(result.text) } | |
| 60 | + } catch { | |
| 61 | + print("error: \(error)") | |
| 62 | + } | |
| 63 | + } | |
| 64 | + } | |
| 65 | +} | |
added
MetrikaKit/Sources/ZQData/DataFrame.swift
+163 −0
@@ -0,0 +1,163 @@ | ||
| 1 | +// | |
| 2 | +// DataFrame.swift | |
| 3 | +// Metrika | |
| 4 | +// | |
| 5 | +// Author: Simon-Pierre Boucher | |
| 6 | +// Contact: contact@spboucher.ai | |
| 7 | +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved. | |
| 8 | +// | |
| 9 | + | |
| 10 | +/// Column payload. Numeric data is Float64 end-to-end (CLAUDE.md §5: all | |
| 11 | +/// CPU compute in Float64) with an explicit validity mask mirroring Arrow | |
| 12 | +/// semantics: `missing[i] == true` means observation `i` is missing, and | |
| 13 | +/// the corresponding value slot is undefined. | |
| 14 | +public enum ZQColumnData: Equatable, Sendable { | |
| 15 | + case float64(values: [Double], missing: [Bool]) | |
| 16 | + case string([String?]) | |
| 17 | + | |
| 18 | + public var count: Int { | |
| 19 | + switch self { | |
| 20 | + case .float64(let values, _): return values.count | |
| 21 | + case .string(let values): return values.count | |
| 22 | + } | |
| 23 | + } | |
| 24 | + | |
| 25 | + public var missingCount: Int { | |
| 26 | + switch self { | |
| 27 | + case .float64(_, let missing): return missing.count { $0 } | |
| 28 | + case .string(let values): return values.count { $0 == nil } | |
| 29 | + } | |
| 30 | + } | |
| 31 | + | |
| 32 | + public var isNumeric: Bool { | |
| 33 | + if case .float64 = self { return true } | |
| 34 | + return false | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +/// A named column of a data frame. | |
| 39 | +public struct ZQColumn: Equatable, Sendable { | |
| 40 | + public var name: String | |
| 41 | + public var data: ZQColumnData | |
| 42 | + | |
| 43 | + public init(name: String, data: ZQColumnData) { | |
| 44 | + self.name = name | |
| 45 | + self.data = data | |
| 46 | + } | |
| 47 | + | |
| 48 | + public var count: Int { data.count } | |
| 49 | + | |
| 50 | + /// Numeric view: values with their validity mask, or nil for string | |
| 51 | + /// columns. | |
| 52 | + public var numeric: (values: [Double], missing: [Bool])? { | |
| 53 | + if case .float64(let values, let missing) = data { | |
| 54 | + return (values, missing) | |
| 55 | + } | |
| 56 | + return nil | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +public struct ZQDataError: Error, Equatable, Sendable, CustomStringConvertible { | |
| 61 | + public var message: String | |
| 62 | + public init(_ message: String) { self.message = message } | |
| 63 | + public var description: String { message } | |
| 64 | +} | |
| 65 | + | |
| 66 | +/// In-memory data frame: the working dataset of a session. Materialized | |
| 67 | +/// from DuckDB query results; columnar, immutable value semantics. | |
| 68 | +public struct ZQDataFrame: Equatable, Sendable { | |
| 69 | + public private(set) var columns: [ZQColumn] | |
| 70 | + public let rowCount: Int | |
| 71 | + | |
| 72 | + public init(columns: [ZQColumn] = []) throws(ZQDataError) { | |
| 73 | + let counts = Set(columns.map(\.count)) | |
| 74 | + guard counts.count <= 1 else { | |
| 75 | + throw ZQDataError("columns have inconsistent lengths: \(counts.sorted())") | |
| 76 | + } | |
| 77 | + self.columns = columns | |
| 78 | + self.rowCount = counts.first ?? 0 | |
| 79 | + } | |
| 80 | + | |
| 81 | + public var isEmpty: Bool { columns.isEmpty || rowCount == 0 } | |
| 82 | + public var columnNames: [String] { columns.map(\.name) } | |
| 83 | + | |
| 84 | + public func column(named name: String) -> ZQColumn? { | |
| 85 | + columns.first { $0.name == name } | |
| 86 | + } | |
| 87 | + | |
| 88 | + public func requireColumn(_ name: String) throws(ZQDataError) -> ZQColumn { | |
| 89 | + guard let column = column(named: name) else { | |
| 90 | + throw ZQDataError("variable '\(name)' not found") | |
| 91 | + } | |
| 92 | + return column | |
| 93 | + } | |
| 94 | + | |
| 95 | + public func requireNumeric( | |
| 96 | + _ name: String | |
| 97 | + ) throws(ZQDataError) -> (values: [Double], missing: [Bool]) { | |
| 98 | + guard let numeric = try requireColumn(name).numeric else { | |
| 99 | + throw ZQDataError("variable '\(name)' is a string variable") | |
| 100 | + } | |
| 101 | + return numeric | |
| 102 | + } | |
| 103 | + | |
| 104 | + /// Adds a new column; fails if the name already exists (Stata `generate` | |
| 105 | + /// semantics — use `replaceColumn` for `replace`). | |
| 106 | + public mutating func addColumn(_ column: ZQColumn) throws(ZQDataError) { | |
| 107 | + guard self.column(named: column.name) == nil else { | |
| 108 | + throw ZQDataError("variable '\(column.name)' already defined") | |
| 109 | + } | |
| 110 | + guard columns.isEmpty || column.count == rowCount else { | |
| 111 | + throw ZQDataError( | |
| 112 | + "column '\(column.name)' has \(column.count) rows, expected \(rowCount)" | |
| 113 | + ) | |
| 114 | + } | |
| 115 | + columns.append(column) | |
| 116 | + } | |
| 117 | + | |
| 118 | + /// Replaces an existing column in place (Stata `replace`). | |
| 119 | + public mutating func replaceColumn(_ column: ZQColumn) throws(ZQDataError) { | |
| 120 | + guard let index = columns.firstIndex(where: { $0.name == column.name }) else { | |
| 121 | + throw ZQDataError("variable '\(column.name)' not found") | |
| 122 | + } | |
| 123 | + guard column.count == rowCount else { | |
| 124 | + throw ZQDataError( | |
| 125 | + "column '\(column.name)' has \(column.count) rows, expected \(rowCount)" | |
| 126 | + ) | |
| 127 | + } | |
| 128 | + columns[index] = column | |
| 129 | + } | |
| 130 | + | |
| 131 | + public mutating func dropColumns(_ names: [String]) throws(ZQDataError) { | |
| 132 | + for name in names { | |
| 133 | + guard columns.contains(where: { $0.name == name }) else { | |
| 134 | + throw ZQDataError("variable '\(name)' not found") | |
| 135 | + } | |
| 136 | + } | |
| 137 | + columns.removeAll { names.contains($0.name) } | |
| 138 | + } | |
| 139 | + | |
| 140 | + /// Row subset by keep-mask (`mask[i] == true` keeps observation `i`). | |
| 141 | + public func filtered(by mask: [Bool]) throws(ZQDataError) -> ZQDataFrame { | |
| 142 | + guard mask.count == rowCount else { | |
| 143 | + throw ZQDataError("filter mask length \(mask.count) != \(rowCount) rows") | |
| 144 | + } | |
| 145 | + let kept = (0..<rowCount).filter { mask[$0] } | |
| 146 | + let newColumns = columns.map { column -> ZQColumn in | |
| 147 | + switch column.data { | |
| 148 | + case .float64(let values, let missing): | |
| 149 | + return ZQColumn( | |
| 150 | + name: column.name, | |
| 151 | + data: .float64( | |
| 152 | + values: kept.map { values[$0] }, | |
| 153 | + missing: kept.map { missing[$0] } | |
| 154 | + ) | |
| 155 | + ) | |
| 156 | + case .string(let values): | |
| 157 | + return ZQColumn(name: column.name, data: .string(kept.map { values[$0] })) | |
| 158 | + } | |
| 159 | + } | |
| 160 | + // Lengths are uniform by construction. | |
| 161 | + return try! ZQDataFrame(columns: newColumns) | |
| 162 | + } | |
| 163 | +} | |
added
MetrikaKit/Sources/ZQData/DuckDBStore.swift
+153 −0
Large diff (153 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQData/ZQDataModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQEngine/ExpressionEvaluator.swift
+332 −0
Large diff (332 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQEngine/Session.swift
+956 −0
Large diff (956 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQEngine/TableFormatter.swift
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQEngine/ZQEngineModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQGPU/Philox.swift
+101 −0
Large diff (101 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQGPU/ZQGPUModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQGraphics/PlotSpec.swift
+43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQGraphics/ZQGraphicsModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/AST.swift
+167 −0
Large diff (167 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/Grammar/CommandParser.swift
+480 −0
Large diff (480 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/Grammar/ExpressionParser.swift
+128 −0
Large diff (128 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/KnownVerbs.swift
+109 −0
Large diff (109 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/Lexer.swift
+169 −0
Large diff (169 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/ParseError.swift
+52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/Token.swift
+55 −0
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQParser/ZQParserModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQPlanner/Planner.swift
+106 −0
Large diff (106 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQPlanner/ZQPlannerModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQPlugins/PluginProtocol.swift
+128 −0
Large diff (128 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQPlugins/ZQPluginsModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQStats/Distributions.swift
+149 −0
Large diff (149 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQStats/LinearAlgebra.swift
+199 −0
Large diff (199 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQStats/OLS.swift
+316 −0
Large diff (316 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQStats/Summarize.swift
+121 −0
Large diff (121 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Sources/ZQStats/ZQStatsModule.swift
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Tests/MetrikaKitTests/EngineTests.swift
+147 −0
Large diff (147 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Tests/MetrikaKitTests/FixtureSupport.swift
+61 −0
Large diff (61 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Tests/MetrikaKitTests/Fixtures/expected.tsv
+47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Tests/MetrikaKitTests/Fixtures/regression.csv
+61 −0
Large diff (61 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Tests/MetrikaKitTests/ParserTests.swift
+174 −0
Large diff (174 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Tests/MetrikaKitTests/PhiloxTests.swift
+92 −0
Large diff (92 lines) is not loaded by default to keep the page fast.Load diff
added
MetrikaKit/Tests/MetrikaKitTests/StatsTests.swift
+218 −0
Large diff (218 lines) is not loaded by default to keep the page fast.Load diff
added
Tests/Bench/bench.sh
+54 −0
Large diff (54 lines) is not loaded by default to keep the page fast.Load diff
added
Tests/Fixtures/generate.R
+161 −0
Large diff (161 lines) is not loaded by default to keep the page fast.Load diff
added
project.yml
+37 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/ExportOptions.plist
+9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/check_headers.sh
+45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/install_hooks.sh
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/make_icns.sh
+41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/pre-commit
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/release.sh
+59 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff