SPB Git

spb/metrika

Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

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>
simon-pierre boucher committed (Aug 5, 2026) Browse files

69 changed files +6,758 −0

69 changed files
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
Menu

Theme