diff --git a/AppSupport/Info.plist b/AppSupport/Info.plist index 9cf2470..31c84ac 100644 --- a/AppSupport/Info.plist +++ b/AppSupport/Info.plist @@ -1,6 +1,6 @@ SUFeedURL - https://grammagem.app/appcast.xml + https://www.grammargem.com/appcast.xml SUPublicEDKey IwYch+DRgNk3gmCwe86z+SIc4sRdyDf4Z6xGpS+JXDc= SUEnableAutomaticChecks diff --git a/Package.swift b/Package.swift index 4eb49a7..bceb8a5 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,7 @@ import PackageDescription import Foundation -// GrammaGem — native macOS menu-bar writing assistant. +// GrammarGem — native macOS menu-bar writing assistant. // // Layer-1 grammar is the real **Harper** core (Apache-2.0), embedded as a Rust // C-FFI static library (see `harper-ffi/`). Build the lib first with @@ -18,41 +18,41 @@ let packageRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().pa let harperLibDir = packageRoot + "/harper-ffi/lib" let package = Package( - name: "GrammaGem", + name: "GrammarGem", platforms: [ .macOS(.v14) // Apple Silicon, macOS 14+ (per the product spec) ], products: [ - .executable(name: "GrammaGem", targets: ["GrammaGem"]) + .executable(name: "GrammarGem", targets: ["GrammarGem"]) ], dependencies: [ // On-device LLM runtime. Pinned to an exact tag (the LLM libraries still // ship from this repo at 2.25.9; the package name is "mlx-libraries"). .package(url: "https://github.com/ml-explore/mlx-swift-examples.git", exact: "2.25.9"), - // Secure auto-updates (EdDSA-signed appcast served from grammagem.app). + // Secure auto-updates (EdDSA-signed appcast served from grammargem.com). .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.0"), ], targets: [ // C shim exposing libharper_ffi's C ABI (harper-ffi/include/harper.h) to Swift. .target(name: "CHarper", path: "Sources/CHarper"), .executableTarget( - name: "GrammaGem", + name: "GrammarGem", dependencies: [ "CHarper", .product(name: "MLXLLM", package: "mlx-swift-examples"), .product(name: "MLXLMCommon", package: "mlx-swift-examples"), .product(name: "Sparkle", package: "Sparkle"), ], - path: "Sources/GrammaGem", + path: "Sources/GrammarGem", linkerSettings: [ // Link the prebuilt Harper static library (arm64 slice). .unsafeFlags(["-L\(harperLibDir)", "-lharper_ffi"]) ] ), .testTarget( - name: "GrammaGemTests", - dependencies: ["GrammaGem"], - path: "Tests/GrammaGemTests" + name: "GrammarGemTests", + dependencies: ["GrammarGem"], + path: "Tests/GrammarGemTests" ), ] ) diff --git a/README.md b/README.md index c4b5b01..5a7a26e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-# ✒️ GrammaGem +# ✒️ GrammarGem ### Write it once. Get it right. @@ -12,7 +12,7 @@ Fix grammar, sharpen tone, and rewrite anything — in every app — for one pay ![One-time purchase](https://img.shields.io/badge/pricing-one--time-C9A24B) ![License: MIT](https://img.shields.io/badge/license-MIT-0E7C5A) -[**⬇️ Download GrammaGem**](https://grammagem.app/#pricing) · [Docs](https://grammagem.app/docs) · [How it works](https://grammagem.app) +[**⬇️ Download GrammarGem**](https://www.grammargem.com/#pricing) · [Docs](https://www.grammargem.com/docs) · [How it works](https://www.grammargem.com)
@@ -20,7 +20,7 @@ Fix grammar, sharpen tone, and rewrite anything — in every app — for one pay ## Stop renting your grammar checker. -Grammarly costs around **$144 every year**, reads everything you type, and sends it to their servers. GrammaGem does the same job — **better** — entirely on your Mac, for a **single payment**. +Grammarly costs around **$144 every year**, reads everything you type, and sends it to their servers. GrammarGem does the same job — **better** — entirely on your Mac, for a **single payment**. Select text in any app, press one hotkey, and it’s polished in place. No copy-paste, no switching windows, and **nothing ever leaves your machine**. @@ -30,7 +30,7 @@ Select text in any app, press one hotkey, and it’s polished in place. No copy- ## Why people switch -| | Grammarly | **GrammaGem** | +| | Grammarly | **GrammarGem** | |---|:---:|:---:| | **Price** | ~$144 / year, forever | **Pay once** | | **Your words** | Sent to the cloud | **Never leave your Mac** | @@ -46,7 +46,7 @@ Select text in any app, press one hotkey, and it’s polished in place. No copy- - 🔒 **Private by design** — everything runs on your Mac’s Neural Engine. Your writing is never uploaded, logged, or used to train a model. - ⌨️ **One global hotkey** — fix or rewrite the selected text in any app: Mail, Slack, Notion, Chrome, Messages, VS Code, and more. - ✨ **Ask & rewrite** — “make it shorter,” “more formal,” “translate to German” — done in place. -- 🎚️ **Writing Modes** — Email, Post, Team Chat, Academic, Code — switch the tone instantly, or let GrammaGem pick the right one for the app you’re in. +- 🎚️ **Writing Modes** — Email, Post, Team Chat, Academic, Code — switch the tone instantly, or let GrammarGem pick the right one for the app you’re in. - 📖 **Personal dictionary** — teach it your names, brand terms, and jargon once; it stops “correcting” them forever. - 🖥️ **A clean management app** — manage your devices, shortcuts, modes, and privacy from one tidy window. @@ -67,7 +67,7 @@ No subscription. No per-seat fees. One payment, lifetime updates.
-[**⬇️ Get GrammaGem**](https://grammagem.app/#pricing) +[**⬇️ Get GrammarGem**](https://www.grammargem.com/#pricing)
@@ -85,17 +85,17 @@ No. Core features work fully offline. No — it’s a one-time purchase with lifetime updates and a 14-day money-back guarantee. **What about plagiarism detection?** -GrammaGem is private-first, so it doesn’t do cloud plagiarism scanning — that would require sending your writing to a server. Everything else you’d expect is here. +GrammarGem is private-first, so it doesn’t do cloud plagiarism scanning — that would require sending your writing to a server. Everything else you’d expect is here. --- ## Built in public -GrammaGem is open source under the **MIT License**. We believe a privacy tool should be one you can actually inspect. Issues and pull requests are welcome. +GrammarGem is open source under the **MIT License**. We believe a privacy tool should be one you can actually inspect. Issues and pull requests are welcome.
-**[grammagem.app](https://grammagem.app)** +**[grammargem.com](https://www.grammargem.com)** © 2026 FounderGem LLC · 1309 Coffeen Avenue STE 1200, Sheridan, WY 82801, USA diff --git a/Resources/Modes/README.md b/Resources/Modes/README.md index 3f56204..f343466 100644 --- a/Resources/Modes/README.md +++ b/Resources/Modes/README.md @@ -1,7 +1,7 @@ # Built-in Writing Modes Each Mode is a named system-prompt preset for the local LLM. The canonical, -compiled definitions live in [`Sources/GrammaGem/AI/ModeRegistry.swift`](../../Sources/GrammaGem/AI/ModeRegistry.swift); +compiled definitions live in [`Sources/GrammarGem/AI/ModeRegistry.swift`](../../Sources/GrammarGem/AI/ModeRegistry.swift); these JSON files mirror them for transparency and easy editing. | Mode | Tier | In public repo? | diff --git a/Sources/CHarper/shim.c b/Sources/CHarper/shim.c index 5089f89..4b76007 100644 --- a/Sources/CHarper/shim.c +++ b/Sources/CHarper/shim.c @@ -1,4 +1,4 @@ // CHarper is a header-only shim that exposes libharper_ffi's C ABI (declared in // include/harper.h) to Swift as the `CHarper` module. The actual symbols are -// provided by libharper_ffi.a, linked via the GrammaGem target's linkerSettings. +// provided by libharper_ffi.a, linked via the GrammarGem target's linkerSettings. // This translation unit exists only so SwiftPM treats CHarper as a C target. diff --git a/Sources/GrammarGem/AI/CustomModesStore.swift b/Sources/GrammarGem/AI/CustomModesStore.swift index 9c15c09..76629fc 100644 --- a/Sources/GrammarGem/AI/CustomModesStore.swift +++ b/Sources/GrammarGem/AI/CustomModesStore.swift @@ -8,8 +8,8 @@ final class CustomModesStore: ObservableObject { /// bundleID -> mode id (overrides the default App-Aware mapping). @Published private(set) var appOverrides: [String: String] - private let modesKey = "GrammaGem.customModes" - private let mapKey = "GrammaGem.appModeOverrides" + private let modesKey = "GrammarGem.customModes" + private let mapKey = "GrammarGem.appModeOverrides" private let d = UserDefaults.standard init() { diff --git a/Sources/GrammarGem/AI/ModelManager.swift b/Sources/GrammarGem/AI/ModelManager.swift index 0402aff..c30d67c 100644 --- a/Sources/GrammarGem/AI/ModelManager.swift +++ b/Sources/GrammarGem/AI/ModelManager.swift @@ -38,7 +38,7 @@ final class ModelManager: ObservableObject { nonisolated static func modelsDirectory() -> URL { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] - return base.appendingPathComponent("GrammaGem/Models", isDirectory: true) + return base.appendingPathComponent("GrammarGem/Models", isDirectory: true) } nonisolated static func modelDir(_ repo: String) -> URL { diff --git a/Sources/GrammarGem/App/AppState.swift b/Sources/GrammarGem/App/AppState.swift index 66d0bb0..6d8b2d5 100644 --- a/Sources/GrammarGem/App/AppState.swift +++ b/Sources/GrammarGem/App/AppState.swift @@ -115,7 +115,7 @@ final class AppState: ObservableObject { } Task { await license.validateIfNeeded() } - Log.app.info("GrammaGem started. Licensed: \(self.license.isLicensed, privacy: .public)") + Log.app.info("GrammarGem started. Licensed: \(self.license.isLicensed, privacy: .public)") } // MARK: - Hotkey handling @@ -205,19 +205,19 @@ final class AppState: ObservableObject { return decision } - /// Pause/resume all of GrammaGem (hotkeys + live monitoring). + /// Pause/resume all of GrammarGem (hotkeys + live monitoring). func togglePause() { isPaused.toggle() liveMonitor.setPaused(isPaused) lastStatus = isPaused ? "Paused" : "Ready" } - /// A reason GrammaGem should stay out of the way right now, or nil. + /// A reason GrammarGem should stay out of the way right now, or nil. private func blockReason() -> String? { - if isPaused { return "GrammaGem is paused." } + if isPaused { return "GrammarGem is paused." } let front = detector.frontmost() if exclusions.isBlocked(bundleID: front?.bundleID, domain: detector.frontmostDomain()) { - return "GrammaGem is off for \(front?.name ?? "this app")." + return "GrammarGem is off for \(front?.name ?? "this app")." } return nil } @@ -282,7 +282,7 @@ final class AppState: ObservableObject { if onboardingWindow == nil { let host = NSHostingController(rootView: OnboardingView().environmentObject(self)) let win = NSWindow(contentViewController: host) - win.title = "Welcome to GrammaGem" + win.title = "Welcome to GrammarGem" win.styleMask = [.titled, .closable, .fullSizeContentView] win.setContentSize(NSSize(width: 560, height: 720)) win.isReleasedWhenClosed = false @@ -311,7 +311,7 @@ final class AppState: ObservableObject { .environmentObject(liveMonitor) let host = NSHostingController(rootView: root) let win = NSWindow(contentViewController: host) - win.title = "GrammaGem" + win.title = "GrammarGem" win.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView] win.setContentSize(NSSize(width: 940, height: 660)) win.minSize = NSSize(width: 820, height: 560) @@ -349,7 +349,7 @@ final class AppState: ObservableObject { if askWindow == nil { let host = NSHostingController(rootView: AskView().environmentObject(self)) let win = NSWindow(contentViewController: host) - win.title = "Ask GrammaGem" + win.title = "Ask GrammarGem" win.styleMask = [.titled, .closable] win.setContentSize(NSSize(width: 460, height: 220)) win.isReleasedWhenClosed = false diff --git a/Sources/GrammarGem/App/AskView.swift b/Sources/GrammarGem/App/AskView.swift index ce02754..618c873 100644 --- a/Sources/GrammarGem/App/AskView.swift +++ b/Sources/GrammarGem/App/AskView.swift @@ -8,7 +8,7 @@ struct AskView: View { var body: some View { VStack(alignment: .leading, spacing: 14) { - Text("Ask GrammaGem") + Text("Ask GrammarGem") .font(.headline) Text("Acts on the text selected in the frontmost app. Runs on-device.") .font(.caption) diff --git a/Sources/GrammarGem/App/GrammarGemApp.swift b/Sources/GrammarGem/App/GrammarGemApp.swift index 2f0031e..5a2bcf4 100644 --- a/Sources/GrammarGem/App/GrammarGemApp.swift +++ b/Sources/GrammarGem/App/GrammarGemApp.swift @@ -5,7 +5,7 @@ import AppKit /// and runs one real generation (verifies the MLX runtime + metallib headlessly), /// then exits. Otherwise the normal menu-bar app launches. @main -enum GrammaGemMain { +enum GrammarGemMain { static func main() { if CommandLine.arguments.contains("--mlx-selftest") { MLXSelfTest.run() @@ -16,7 +16,7 @@ enum GrammaGemMain { UpdaterSelfTest.run(feedURL: feed) return } - GrammaGemApp.main() + GrammarGemApp.main() } } @@ -47,7 +47,7 @@ enum MLXSelfTest { /// App entry point. A menu-bar (`LSUIElement`) SwiftUI app: the only persistent /// scene is the `MenuBarExtra`; Settings uses the standard scene; Onboarding and /// Ask are AppKit-hosted windows opened on demand (see `AppState`). -struct GrammaGemApp: App { +struct GrammarGemApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @StateObject private var app = AppState.shared diff --git a/Sources/GrammarGem/App/MainWindow/AccountViews.swift b/Sources/GrammarGem/App/MainWindow/AccountViews.swift index 6304145..e77fec2 100644 --- a/Sources/GrammarGem/App/MainWindow/AccountViews.swift +++ b/Sources/GrammarGem/App/MainWindow/AccountViews.swift @@ -8,7 +8,7 @@ struct GeneralView: View { @EnvironmentObject private var permissions: Permissions var body: some View { - DetailScaffold(title: "General", subtitle: "Make GrammaGem feel like yours.") { + DetailScaffold(title: "General", subtitle: "Make GrammarGem feel like yours.") { Card { Toggle("Launch at login", isOn: $prefs.launchAtLogin) Divider() @@ -134,7 +134,7 @@ struct AboutView: View { .frame(width: 56, height: 56) .background(GG.emerald.opacity(0.12), in: RoundedRectangle(cornerRadius: 14)) VStack(alignment: .leading, spacing: 2) { - Text("GrammaGem").font(.title2.bold()) + Text("GrammarGem").font(.title2.bold()) Text("Version \(AppConfig.appVersion)").foregroundStyle(.secondary) Text("The private, on-device writing assistant for Mac.") .font(.callout).foregroundStyle(.secondary) @@ -153,7 +153,7 @@ struct AboutView: View { Card { Text("Open source").font(.headline) - Text("GrammaGem is open source under the MIT License — a privacy tool you can inspect.") + Text("GrammarGem is open source under the MIT License — a privacy tool you can inspect.") .font(.callout).foregroundStyle(.secondary) } diff --git a/Sources/GrammarGem/App/MainWindow/DashboardView.swift b/Sources/GrammarGem/App/MainWindow/DashboardView.swift index 7bdc8b3..b57c192 100644 --- a/Sources/GrammarGem/App/MainWindow/DashboardView.swift +++ b/Sources/GrammarGem/App/MainWindow/DashboardView.swift @@ -91,7 +91,7 @@ struct DashboardView: View { Label("Fix selection", systemImage: "checkmark.circle") } Button { app.showAsk() } label: { - Label("Ask GrammaGem", systemImage: "sparkles") + Label("Ask GrammarGem", systemImage: "sparkles") } if !license.isLicensed { Spacer() diff --git a/Sources/GrammarGem/App/MainWindow/DataViews.swift b/Sources/GrammarGem/App/MainWindow/DataViews.swift index de9c92d..4b35b4b 100644 --- a/Sources/GrammarGem/App/MainWindow/DataViews.swift +++ b/Sources/GrammarGem/App/MainWindow/DataViews.swift @@ -12,7 +12,7 @@ struct DictionaryView: View { var body: some View { DetailScaffold( title: "Personal Dictionary", - subtitle: "Words GrammaGem should never “correct” — names, brands, jargon." + subtitle: "Words GrammarGem should never “correct” — names, brands, jargon." ) { Card { HStack { diff --git a/Sources/GrammarGem/App/MainWindow/DevicesView.swift b/Sources/GrammarGem/App/MainWindow/DevicesView.swift index d2147f9..d39a93e 100644 --- a/Sources/GrammarGem/App/MainWindow/DevicesView.swift +++ b/Sources/GrammarGem/App/MainWindow/DevicesView.swift @@ -83,7 +83,7 @@ struct DevicesView: View { HStack { Link("Open license portal", destination: AppConfig.modelPortalURL) Spacer() - Text("Add a Mac: install GrammaGem there and paste the same key.") + Text("Add a Mac: install GrammarGem there and paste the same key.") .font(.caption).foregroundStyle(.secondary) } } diff --git a/Sources/GrammarGem/App/MainWindow/ExclusionsView.swift b/Sources/GrammarGem/App/MainWindow/ExclusionsView.swift index 74e4667..0c005d8 100644 --- a/Sources/GrammarGem/App/MainWindow/ExclusionsView.swift +++ b/Sources/GrammarGem/App/MainWindow/ExclusionsView.swift @@ -8,12 +8,12 @@ struct ExclusionsView: View { var body: some View { DetailScaffold( title: "Page Blocker", - subtitle: "Keep GrammaGem out of the way in sensitive apps and sites." + subtitle: "Keep GrammarGem out of the way in sensitive apps and sites." ) { Card { Label("How it works", systemImage: "hand.raised") .font(.headline) - Text("When the frontmost app or website is on these lists, GrammaGem stops monitoring and the hotkeys do nothing — useful for password managers, banking, and private work.") + Text("When the frontmost app or website is on these lists, GrammarGem stops monitoring and the hotkeys do nothing — useful for password managers, banking, and private work.") .font(.callout).foregroundStyle(.secondary) } diff --git a/Sources/GrammarGem/App/MainWindow/MainWindowView.swift b/Sources/GrammarGem/App/MainWindow/MainWindowView.swift index bc81ab6..2b2d224 100644 --- a/Sources/GrammarGem/App/MainWindow/MainWindowView.swift +++ b/Sources/GrammarGem/App/MainWindow/MainWindowView.swift @@ -59,7 +59,7 @@ enum MainSection: String, CaseIterable, Identifiable { ] } -/// The full GrammaGem management window: a clean sidebar + detail layout. +/// The full GrammarGem management window: a clean sidebar + detail layout. struct MainWindowView: View { @State private var selection: MainSection? diff --git a/Sources/GrammarGem/App/MainWindow/SystemViews.swift b/Sources/GrammarGem/App/MainWindow/SystemViews.swift index 41b5d08..bec3bb8 100644 --- a/Sources/GrammarGem/App/MainWindow/SystemViews.swift +++ b/Sources/GrammarGem/App/MainWindow/SystemViews.swift @@ -17,7 +17,7 @@ struct ShortcutsView: View { combo: $prefs.fixHotkey) Divider() shortcutRow( - name: "Ask GrammaGem", + name: "Ask GrammarGem", detail: "Rewrite with an instruction", combo: $prefs.askHotkey) } @@ -141,7 +141,7 @@ struct PrivacyView: View { } Card { - Text("The only times GrammaGem uses the network").font(.headline) + Text("The only times GrammarGem uses the network").font(.headline) networkRow("One-time AI model download", "Hugging Face, on first run") Divider() networkRow("License activation & checks", "To enforce your device cap") diff --git a/Sources/GrammarGem/App/MainWindow/WritingModesView.swift b/Sources/GrammarGem/App/MainWindow/WritingModesView.swift index e25b9f2..f981b27 100644 --- a/Sources/GrammarGem/App/MainWindow/WritingModesView.swift +++ b/Sources/GrammarGem/App/MainWindow/WritingModesView.swift @@ -15,7 +15,7 @@ struct WritingModesView: View { var body: some View { DetailScaffold( title: "Writing Modes", - subtitle: "Tune the tone for every context — or let GrammaGem pick it for you." + subtitle: "Tune the tone for every context — or let GrammarGem pick it for you." ) { builtInCard customCard @@ -88,7 +88,7 @@ struct WritingModesView: View { private var appAwareCard: some View { Card { HStack { Text("App-aware switching").font(.headline); if !paid { UpgradeBadge() } } - Text("Map an app to a Mode and GrammaGem applies it automatically when that app is in front.") + Text("Map an app to a Mode and GrammarGem applies it automatically when that app is in front.") .font(.callout).foregroundStyle(.secondary) if !customModes.appOverrides.isEmpty { diff --git a/Sources/GrammarGem/App/MenuBarContent.swift b/Sources/GrammarGem/App/MenuBarContent.swift index 053e3dd..4c00415 100644 --- a/Sources/GrammarGem/App/MenuBarContent.swift +++ b/Sources/GrammarGem/App/MenuBarContent.swift @@ -49,7 +49,7 @@ struct MenuBarContent: View { actionRow(title: "Fix selection", shortcut: app.preferences.fixHotkey.display) { Task { await app.runFix() } } - actionRow(title: "Ask GrammaGem…", shortcut: app.preferences.askHotkey.display) { + actionRow(title: "Ask GrammarGem…", shortcut: app.preferences.askHotkey.display) { app.showAsk() } if app.gate.appAwareEnabled { @@ -57,14 +57,14 @@ struct MenuBarContent: View { Task { await app.runAppAwareRewrite() } } } - actionRow(title: app.isPaused ? "Resume GrammaGem" : "Pause GrammaGem", shortcut: nil) { + actionRow(title: app.isPaused ? "Resume GrammarGem" : "Pause GrammarGem", shortcut: nil) { app.togglePause() } Divider() actionRow(title: "Show live issues…", shortcut: nil) { app.showLiveCheck() } - actionRow(title: "Open GrammaGem…", shortcut: nil) { app.showMainWindow() } + actionRow(title: "Open GrammarGem…", shortcut: nil) { app.showMainWindow() } actionRow(title: "Manage devices…", shortcut: nil) { app.showMainWindow(select: .devices) } actionRow(title: "Page blocker…", shortcut: nil) { app.showMainWindow(select: .exclusions) } actionRow(title: "Check for Updates…", shortcut: nil) { app.updater.checkForUpdates() } @@ -90,7 +90,7 @@ struct MenuBarContent: View { .frame(width: 290) } - /// Shown when Sparkle has found a newer signed build on grammagem.app. + /// Shown when Sparkle has found a newer signed build on grammargem.com. private func updateBanner(version: String) -> some View { HStack(spacing: 8) { Image(systemName: "arrow.down.circle.fill").foregroundStyle(GG.emerald) @@ -110,7 +110,7 @@ struct MenuBarContent: View { HStack(spacing: 8) { Image(systemName: "pencil.and.scribble").foregroundStyle(.tint) VStack(alignment: .leading, spacing: 1) { - Text("GrammaGem").font(.headline) + Text("GrammarGem").font(.headline) Text(app.license.tier.displayName + (app.license.isLicensed ? " · licensed" : " · free")) .font(.caption).foregroundStyle(.secondary) } diff --git a/Sources/GrammarGem/App/OnboardingView.swift b/Sources/GrammarGem/App/OnboardingView.swift index 8527961..b926043 100644 --- a/Sources/GrammarGem/App/OnboardingView.swift +++ b/Sources/GrammarGem/App/OnboardingView.swift @@ -1,6 +1,6 @@ import SwiftUI -/// First-run onboarding: explains why GrammaGem needs Accessibility, deep-links +/// First-run onboarding: explains why GrammarGem needs Accessibility, deep-links /// to System Settings, and advances automatically the moment permission is /// granted (even if granted directly in System Settings). struct OnboardingView: View { @@ -18,7 +18,7 @@ struct OnboardingView: View { ScrollView { VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 6) { - Text("Welcome to GrammaGem") + Text("Welcome to GrammarGem") .font(.largeTitle.bold()) Text("A private, on-device writing assistant that works in every app.") .foregroundStyle(.secondary) @@ -36,7 +36,7 @@ struct OnboardingView: View { stepCard( number: 1, title: "Turn it on everywhere", - body: "GrammaGem reads the text you select and writes the correction back. macOS requires Accessibility permission for this. Your text is processed on-device and never leaves your Mac.", + body: "GrammarGem reads the text you select and writes the correction back. macOS requires Accessibility permission for this. Your text is processed on-device and never leaves your Mac.", granted: permissions.accessibilityTrusted ) { if permissions.accessibilityTrusted { @@ -47,7 +47,7 @@ struct OnboardingView: View { Button("Open System Settings") { permissions.openAccessibilitySettings() } Button("Request permission") { permissions.requestAccessibility() } } - Text("This updates automatically once you flip GrammaGem on in Accessibility.") + Text("This updates automatically once you flip GrammarGem on in Accessibility.") .font(.caption).foregroundStyle(.secondary) } } @@ -114,7 +114,7 @@ struct OnboardingView: View { /// A safe, on-rails first-run demo: a local sample with planted mistakes that /// the REAL grammar engine fixes in one click. It operates on a plain string /// (never a live Accessibility element), so it's completely risk-free — the -/// "guaranteed visible win" before GrammaGem asks for anything. +/// "guaranteed visible win" before GrammarGem asks for anything. private struct DemoStep: View { @State private var text = "i has wrote alot of mistakes here, and it dont look profesional" @State private var issues: [Suggestion] = [] @@ -151,7 +151,7 @@ private struct DemoStep: View { } private var headline: String { - if cleaned { return "Clean ✨ — GrammaGem does this in every app you type." } + if cleaned { return "Clean ✨ — GrammarGem does this in every app you type." } if issues.isEmpty { return "Checking…" } return "We spotted \(issues.count) thing\(issues.count == 1 ? "" : "s") to tidy up — one click fixes them." } diff --git a/Sources/GrammarGem/App/SettingsView.swift b/Sources/GrammarGem/App/SettingsView.swift index 32981b9..b293065 100644 --- a/Sources/GrammarGem/App/SettingsView.swift +++ b/Sources/GrammarGem/App/SettingsView.swift @@ -25,7 +25,7 @@ private struct GeneralSettings: View { Form { Section("Shortcuts") { LabeledContent("Fix selection", value: "⌘;") - LabeledContent("Ask GrammaGem", value: "⌘'") + LabeledContent("Ask GrammarGem", value: "⌘'") Text("Custom shortcuts coming via the KeyboardShortcuts recorder (see README).") .font(.caption).foregroundStyle(.secondary) } @@ -147,7 +147,7 @@ private struct DictionarySettings: View { var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { - TextField("Add a word GrammaGem should never correct", text: $newWord) + TextField("Add a word GrammarGem should never correct", text: $newWord) .textFieldStyle(.roundedBorder) .onSubmit(add) Button("Add", action: add).disabled(newWord.trimmingCharacters(in: .whitespaces).isEmpty) diff --git a/Sources/GrammarGem/Core/AppConfig.swift b/Sources/GrammarGem/Core/AppConfig.swift index a1016a2..7df8bab 100644 --- a/Sources/GrammarGem/Core/AppConfig.swift +++ b/Sources/GrammarGem/Core/AppConfig.swift @@ -1,15 +1,15 @@ import Foundation /// Compile-time configuration. The Lemon Squeezy identifiers are hard-coded so -/// the licensing layer can *hard-verify* that a key belongs to GrammaGem (and to +/// the licensing layer can *hard-verify* that a key belongs to GrammarGem (and to /// the claimed tier) rather than to some other Lemon Squeezy product. /// TODO(real-integration): replace every `TODO_*` value with your real IDs. enum AppConfig { - static let appName = "GrammaGem" - static let bundleIdentifier = "com.foundergem.grammagem" // TODO(real-integration) - static let supportEmail = "support@grammagem.app" // TODO(real-integration) - static let modelPortalURL = URL(string: "https://grammagem.app/license")! // TODO - static let websiteURL = URL(string: "https://grammagem.app")! + static let appName = "GrammarGem" + static let bundleIdentifier = "com.foundergem.grammargem" // TODO(real-integration) + static let supportEmail = "support@grammargem.com" // TODO(real-integration) + static let modelPortalURL = URL(string: "https://www.grammargem.com/license")! // TODO + static let websiteURL = URL(string: "https://www.grammargem.com")! static let appVersion = "0.1.0" // Legal entity (shown in About / legal). diff --git a/Sources/GrammarGem/Entitlements/FeatureGate.swift b/Sources/GrammarGem/Entitlements/FeatureGate.swift index 981a02c..13e56ef 100644 --- a/Sources/GrammarGem/Entitlements/FeatureGate.swift +++ b/Sources/GrammarGem/Entitlements/FeatureGate.swift @@ -72,7 +72,7 @@ final class FeatureGate: ObservableObject { /// Free-tier AI action counter. Persists `{date, count}` in UserDefaults and /// resets at local midnight (compares the stored local date string to today's). final class DailyCounter { - private let key = "GrammaGem.aiActions.daily" + private let key = "GrammarGem.aiActions.daily" private let defaults = UserDefaults.standard private static let formatter: DateFormatter = { diff --git a/Sources/GrammarGem/Grammar/PersonalDictionary.swift b/Sources/GrammarGem/Grammar/PersonalDictionary.swift index 97764ac..29afb66 100644 --- a/Sources/GrammarGem/Grammar/PersonalDictionary.swift +++ b/Sources/GrammarGem/Grammar/PersonalDictionary.swift @@ -1,13 +1,13 @@ import Foundation -/// The user's personal dictionary — names, brand terms, jargon GrammaGem should +/// The user's personal dictionary — names, brand terms, jargon GrammarGem should /// never "correct". Feeds Harper's ignore-list and the LLM context. Capped at 25 /// entries on free, unlimited on paid (enforced by `FeatureGate`, not here). @MainActor final class PersonalDictionary: ObservableObject { @Published private(set) var entries: [String] - private let key = "GrammaGem.personalDictionary" + private let key = "GrammarGem.personalDictionary" private let defaults = UserDefaults.standard init() { diff --git a/Sources/GrammarGem/Licensing/DeviceFingerprint.swift b/Sources/GrammarGem/Licensing/DeviceFingerprint.swift index be5ae2b..b17ee51 100644 --- a/Sources/GrammarGem/Licensing/DeviceFingerprint.swift +++ b/Sources/GrammarGem/Licensing/DeviceFingerprint.swift @@ -38,7 +38,7 @@ enum DeviceFingerprint { } private static func fallbackID() -> String { - let key = "GrammaGem.fallbackDeviceID" + let key = "GrammarGem.fallbackDeviceID" if let existing = UserDefaults.standard.string(forKey: key) { return existing } let id = UUID().uuidString UserDefaults.standard.set(id, forKey: key) diff --git a/Sources/GrammarGem/Licensing/LemonSqueezyClient.swift b/Sources/GrammarGem/Licensing/LemonSqueezyClient.swift index 5b9a6b4..15f2f69 100644 --- a/Sources/GrammarGem/Licensing/LemonSqueezyClient.swift +++ b/Sources/GrammarGem/Licensing/LemonSqueezyClient.swift @@ -2,7 +2,7 @@ import Foundation /// Talks to the **Lemon Squeezy License API** directly from the app — no server /// of ours, no database, no per-user cost (spec §6). Exposes activate / validate -/// / deactivate and hard-verifies that a key belongs to GrammaGem + the claimed tier. +/// / deactivate and hard-verifies that a key belongs to GrammarGem + the claimed tier. final class LemonSqueezyClient { private let session: URLSession private let base = AppConfig.LemonSqueezy.apiBase @@ -54,7 +54,7 @@ final class LemonSqueezyClient { // MARK: - Hard verification (spec §6) /// Confirm the key is for OUR store/product, then resolve which tier its - /// variant corresponds to. Prevents a key from another LS product unlocking GrammaGem. + /// variant corresponds to. Prevents a key from another LS product unlocking GrammarGem. private func verifyAndResolveTier(meta: LSMeta?, licenseKey: LSLicenseKey?) throws -> Tier { guard let meta else { throw LicenseError.wrongProduct } diff --git a/Sources/GrammarGem/Licensing/LicenseModels.swift b/Sources/GrammarGem/Licensing/LicenseModels.swift index a9196a5..140a3e2 100644 --- a/Sources/GrammarGem/Licensing/LicenseModels.swift +++ b/Sources/GrammarGem/Licensing/LicenseModels.swift @@ -62,7 +62,7 @@ struct LSLicenseKey: Codable { } /// `meta` carries the store/product/variant identity we *hard-verify* against -/// AppConfig so a key from another Lemon Squeezy product can't unlock GrammaGem. +/// AppConfig so a key from another Lemon Squeezy product can't unlock GrammarGem. struct LSMeta: Codable { let store_id: Int? let product_id: Int? diff --git a/Sources/GrammarGem/Stats/UsageStats.swift b/Sources/GrammarGem/Stats/UsageStats.swift index 47e90c4..40d5db0 100644 --- a/Sources/GrammarGem/Stats/UsageStats.swift +++ b/Sources/GrammarGem/Stats/UsageStats.swift @@ -14,12 +14,12 @@ final class UsageStats: ObservableObject { } /// What a Grammarly-style subscription runs per year (USD) — used only for the - /// honest, local "subscription you didn't pay" figure. GrammaGem is one-time. + /// honest, local "subscription you didn't pay" figure. GrammarGem is one-time. static let comparableAnnualSubscriptionUSD = 144.0 @Published private(set) var data: Snapshot - private let key = "GrammaGem.usageStats" + private let key = "GrammarGem.usageStats" private let d = UserDefaults.standard private static let fmt: DateFormatter = { @@ -64,7 +64,7 @@ final class UsageStats: ObservableObject { var currentStreak: Int { Self.streak(perDay: data.perDay, asOf: Date()) } /// The Grammarly subscription you DIDN'T pay: their annual price prorated over - /// how long you've owned GrammaGem. Honest, local, illustrative — and it only + /// how long you've owned GrammarGem. Honest, local, illustrative — and it only /// grows, which is the point of a one-time purchase (see the value plan §5). var subscriptionAvoidedUSD: Int { Self.subscriptionAvoidedUSD( diff --git a/Sources/GrammarGem/System/AXSupport.swift b/Sources/GrammarGem/System/AXSupport.swift index a80141c..48f155c 100644 --- a/Sources/GrammarGem/System/AXSupport.swift +++ b/Sources/GrammarGem/System/AXSupport.swift @@ -43,7 +43,7 @@ enum AX { /// /// On recent macOS, `AXUIElementCreateSystemWide()` returns /// `kAXErrorCannotComplete` (-25204) for *every* query made by a background / - /// accessory process (which is exactly what a menu-bar app like GrammaGem is). + /// accessory process (which is exactly what a menu-bar app like GrammarGem is). /// When that happens the system-wide focused-element read yields nil and the /// app "sees" no text or text fields at all. Resolving the focused element /// through the **frontmost application's** own AX element works reliably in diff --git a/Sources/GrammarGem/System/Exclusions.swift b/Sources/GrammarGem/System/Exclusions.swift index 4587ed7..84ac71f 100644 --- a/Sources/GrammarGem/System/Exclusions.swift +++ b/Sources/GrammarGem/System/Exclusions.swift @@ -1,7 +1,7 @@ import Foundation import AppKit -/// The "page blocker": apps and domains where GrammaGem should stay completely +/// The "page blocker": apps and domains where GrammarGem should stay completely /// out of the way — password managers, banking sites, anything sensitive. /// When the frontmost context is excluded, live monitoring and the hotkeys are /// suppressed. @@ -10,8 +10,8 @@ final class Exclusions: ObservableObject { @Published private(set) var blockedApps: [String] // bundle identifiers @Published private(set) var blockedDomains: [String] // hostnames, e.g. "chase.com" - private let appsKey = "GrammaGem.blockedApps" - private let domainsKey = "GrammaGem.blockedDomains" + private let appsKey = "GrammarGem.blockedApps" + private let domainsKey = "GrammarGem.blockedDomains" private let d = UserDefaults.standard /// Common sensitive apps offered as one-tap suggestions in the UI. @@ -31,7 +31,7 @@ final class Exclusions: ObservableObject { /// a browser's URL can't be read). var hasDomainRules: Bool { !blockedDomains.isEmpty } - /// Whether GrammaGem should be suppressed for this frontmost context. + /// Whether GrammarGem should be suppressed for this frontmost context. func isBlocked(bundleID: String?, domain: String?) -> Bool { if let bundleID, blockedApps.contains(bundleID) { return true } if let host = domain?.lowercased(), !host.isEmpty { diff --git a/Sources/GrammarGem/System/LiveMonitor.swift b/Sources/GrammarGem/System/LiveMonitor.swift index 2cc22ff..ad6b65c 100644 --- a/Sources/GrammarGem/System/LiveMonitor.swift +++ b/Sources/GrammarGem/System/LiveMonitor.swift @@ -19,7 +19,7 @@ struct DetectedField: Identifiable { /// Performance & safety: the Accessibility-tree traversal (the part that can hang /// on a busy/wedged app) runs OFF the main thread with a per-message timeout; only /// the bounded spell/grammar pass and publish happen on the main actor. It never -/// scans GrammaGem itself, respects the page blocker, and applies fixes against the +/// scans GrammarGem itself, respects the page blocker, and applies fixes against the /// element's *live* value so it can't overwrite text typed since the last scan. @MainActor final class LiveMonitor: ObservableObject { @@ -179,7 +179,7 @@ final class LiveMonitor: ObservableObject { if scanning && !force { return } // don't pile up scans guard let app = NSWorkspace.shared.frontmostApplication else { clear(); return } - // Never scan GrammaGem's own windows. + // Never scan GrammarGem's own windows. if app.processIdentifier == ownPID { clear(); return } activeAppName = app.localizedName ?? "" diff --git a/Sources/GrammarGem/System/Permissions.swift b/Sources/GrammarGem/System/Permissions.swift index 044f44d..3ac8ec2 100644 --- a/Sources/GrammarGem/System/Permissions.swift +++ b/Sources/GrammarGem/System/Permissions.swift @@ -2,7 +2,7 @@ import Foundation import AppKit import ApplicationServices -/// Tracks and requests the macOS permissions GrammaGem needs to read & replace +/// Tracks and requests the macOS permissions GrammarGem needs to read & replace /// text system-wide: **Accessibility** (required) and, for the synthetic-keystroke /// clipboard fallback, **Input Monitoring** may also be prompted by the system. @MainActor diff --git a/Sources/GrammarGem/System/SnippetStore.swift b/Sources/GrammarGem/System/SnippetStore.swift index 4d17914..a959497 100644 --- a/Sources/GrammarGem/System/SnippetStore.swift +++ b/Sources/GrammarGem/System/SnippetStore.swift @@ -13,7 +13,7 @@ struct Snippet: Codable, Identifiable, Equatable { final class SnippetStore: ObservableObject { @Published private(set) var snippets: [Snippet] - private let key = "GrammaGem.snippets" + private let key = "GrammarGem.snippets" private let d = UserDefaults.standard init() { diff --git a/Sources/GrammarGem/System/TextCapture.swift b/Sources/GrammarGem/System/TextCapture.swift index e9df9a2..80e4f48 100644 --- a/Sources/GrammarGem/System/TextCapture.swift +++ b/Sources/GrammarGem/System/TextCapture.swift @@ -4,7 +4,7 @@ import ApplicationServices import Carbon.HIToolbox /// Reads the user's selected text from the frontmost app and writes a corrected -/// value back — the mechanism that makes GrammaGem work *everywhere*. +/// value back — the mechanism that makes GrammarGem work *everywhere*. /// /// Two paths, mirroring the spec (§4): /// 1. **Accessibility API** (preferred): read/write `kAXSelectedTextAttribute` @@ -27,7 +27,7 @@ final class TextCapture { var errorDescription: String? { switch self { case .noSelection: return "Select some text first, then press the hotkey." - case .notPermitted: return "GrammaGem needs Accessibility permission to read and replace text." + case .notPermitted: return "GrammarGem needs Accessibility permission to read and replace text." } } } diff --git a/Sources/GrammarGem/System/TextFieldScanner.swift b/Sources/GrammarGem/System/TextFieldScanner.swift index e2c40fb..ff5ac1d 100644 --- a/Sources/GrammarGem/System/TextFieldScanner.swift +++ b/Sources/GrammarGem/System/TextFieldScanner.swift @@ -15,7 +15,7 @@ struct ScannedField { /// Walks the Accessibility tree of the frontmost window and collects every /// editable text element (text fields, text areas, combo/search boxes) with -/// content — so GrammaGem can check *all* the text on screen, not just the one +/// content — so GrammarGem can check *all* the text on screen, not just the one /// the cursor is in. Bounded so huge web/Electron trees can't stall the app, and /// every cross-process message has a timeout so a wedged app can't hang the scan. /// diff --git a/Sources/GrammarGem/System/UpdaterManager.swift b/Sources/GrammarGem/System/UpdaterManager.swift index c8aac65..e4c2981 100644 --- a/Sources/GrammarGem/System/UpdaterManager.swift +++ b/Sources/GrammarGem/System/UpdaterManager.swift @@ -1,7 +1,7 @@ import Foundation import Sparkle -/// Wraps Sparkle's updater. It background-checks the appcast on grammagem.app and +/// Wraps Sparkle's updater. It background-checks the appcast on grammargem.com and /// publishes `updateAvailableVersion` when a newer, EdDSA-verified build exists, so /// the menu bar can show an "Update available" button. Triggering an update runs /// Sparkle's verified download + install flow. diff --git a/Tests/GrammarGemTests/GrammarGemTests.swift b/Tests/GrammarGemTests/GrammarGemTests.swift index 82f420f..41bc914 100644 --- a/Tests/GrammarGemTests/GrammarGemTests.swift +++ b/Tests/GrammarGemTests/GrammarGemTests.swift @@ -1,7 +1,7 @@ import XCTest -@testable import GrammaGem +@testable import GrammarGem -final class GrammaGemTests: XCTestCase { +final class GrammarGemTests: XCTestCase { // MARK: - Tiers / entitlements @@ -117,10 +117,10 @@ final class GrammaGemTests: XCTestCase { /// MLX integration end-to-end. func testMLXRealGenerationIfModelPresent() async throws { // Opt-in: `swift test` (CLI) can't place mlx-swift's metallib, so this is - // gated. Run it via the app's `--mlx-selftest`, or set GRAMMAGEM_MLX_TEST=1 + // gated. Run it via the app's `--mlx-selftest`, or set GRAMMARGEM_MLX_TEST=1 // with the metallib colocated. See scripts/build-metallib.sh. - guard ProcessInfo.processInfo.environment["GRAMMAGEM_MLX_TEST"] == "1" else { - throw XCTSkip("set GRAMMAGEM_MLX_TEST=1 (metallib colocated) to run the MLX integration test") + guard ProcessInfo.processInfo.environment["GRAMMARGEM_MLX_TEST"] == "1" else { + throw XCTSkip("set GRAMMARGEM_MLX_TEST=1 (metallib colocated) to run the MLX integration test") } guard let dir = ModelManager.completedModelDirectory(repo: AppConfig.Model.defaultRepo) else { throw XCTSkip("on-device model not downloaded; skipping MLX integration test") diff --git a/docs/grammarly-value-plan.md b/docs/grammarly-value-plan.md index a37e54c..11bf563 100644 --- a/docs/grammarly-value-plan.md +++ b/docs/grammarly-value-plan.md @@ -1,6 +1,6 @@ -# Grammarly Value Teardown -> GrammaGem Plan +# Grammarly Value Teardown -> GrammarGem Plan -*Senior product + Swift engineering review. Every claim below is grounded in the current GrammaGem source. File references use absolute paths; the load-bearing bug is the mis-casing in `SystemGrammarEngine.swift:42-45`.* +*Senior product + Swift engineering review. Every claim below is grounded in the current GrammarGem source. File references use absolute paths; the load-bearing bug is the mis-casing in `SystemGrammarEngine.swift:42-45`.* --- @@ -8,34 +8,34 @@ Grammarly's perceived value is NOT its grammar engine. It is a stack of interaction and psychology layers on top of a competent engine. Ranked by leverage for a one-time-purchase, on-device Mac utility: -1. **Trustworthy correction quality (the foundation).** Every felt-value mechanic above it collapses if a fix is wrong. Grammarly's "accept-accept-accept" rhythm only works because in-place application is reliable and casing/context is preserved. GrammaGem's engine currently *demonstrably violates this* (`problemm` -> `Problem` mid-sentence). Fix this first; nothing else matters until it's solid. +1. **Trustworthy correction quality (the foundation).** Every felt-value mechanic above it collapses if a fix is wrong. Grammarly's "accept-accept-accept" rhythm only works because in-place application is reliable and casing/context is preserved. GrammarGem's engine currently *demonstrably violates this* (`problemm` -> `Problem` mid-sentence). Fix this first; nothing else matters until it's solid. -2. **The fix-as-button atomic loop: read the fix and apply it in one motion.** Grammarly's clickable green replacement chip collapses diagnosis + remedy + action into a single low-friction act, with a cheap, always-present dismiss so saying no is as easy as saying yes. GrammaGem has an `Apply` button but no dismiss/ignore/add-to-dictionary, no keyboard accept, and the apply path is brittle (whole-field AX overwrite). +2. **The fix-as-button atomic loop: read the fix and apply it in one motion.** Grammarly's clickable green replacement chip collapses diagnosis + remedy + action into a single low-friction act, with a cheap, always-present dismiss so saying no is as easy as saying yes. GrammarGem has an `Apply` button but no dismiss/ignore/add-to-dictionary, no keyboard accept, and the apply path is brittle (whole-field AX overwrite). -3. **Categorization + counts + a score: turning a wall of red into a finite, beatable list.** Four named, color-coded buckets, per-category count chips, one running issue badge, and a 0-100 score convert dread into a checklist with a meter that only goes up. GrammaGem has a raw issue count (3 colors) and *zero* score concept. +3. **Categorization + counts + a score: turning a wall of red into a finite, beatable list.** Four named, color-coded buckets, per-category count chips, one running issue badge, and a 0-100 score convert dread into a checklist with a meter that only goes up. GrammarGem has a raw issue count (3 colors) and *zero* score concept. -4. **The rigged first-run "wow": guaranteed visible win in under 60 seconds.** A pre-seeded, error-salted demo doc means the product proves itself before asking for anything. GrammaGem's onboarding only handles Accessibility + model download — it never demonstrates the correction loop at all. +4. **The rigged first-run "wow": guaranteed visible win in under 60 seconds.** A pre-seeded, error-salted demo doc means the product proves itself before asking for anything. GrammarGem's onboarding only handles Accessibility + model download — it never demonstrates the correction loop at all. -5. **Ongoing perceived value without surveillance: progress you can see.** Grammarly uses cloud percentiles + weekly behavioral emails. GrammaGem can recreate the *feeling* — self-vs-self, fully on-device — with a local score trend, streaks, milestones, and a "subscription you didn't pay" money figure. The plumbing (`UsageStats.perDay`) already exists. +5. **Ongoing perceived value without surveillance: progress you can see.** Grammarly uses cloud percentiles + weekly behavioral emails. GrammarGem can recreate the *feeling* — self-vs-self, fully on-device — with a local score trend, streaks, milestones, and a "subscription you didn't pay" money figure. The plumbing (`UsageStats.perDay`) already exists. 6. **The generative "writing partner" layer: hand me the better sentence, not just the flag.** Sentence rewrites, tone detection/adjustment, conciseness. This is real value but is currently 100% simulated (`MLXEngine.swift` is a string-hack stub) and gated by a daily counter. It is the right *paid* tier, but it must produce real output before it can be sold. -The strategic asymmetry to exploit throughout: every Grammarly value moment carries an implicit "and you'll pay again next year." GrammaGem's identical mechanics should be framed "yours once, forever, on this Mac." +The strategic asymmetry to exploit throughout: every Grammarly value moment carries an implicit "and you'll pay again next year." GrammarGem's identical mechanics should be framed "yours once, forever, on this Mac." --- -## 2. Mechanic-by-mechanic: how Grammarly does it -> how GrammaGem should do it +## 2. Mechanic-by-mechanic: how Grammarly does it -> how GrammarGem should do it ### Mechanic 1 — Trustworthy correction quality (see §3 for the full engineering plan) - **Grammarly:** ranks by confidence, suppresses low-confidence edits, never garbles surrounding text, preserves casing, and limits bulk-apply to high-confidence objective mechanics only. -- **GrammaGem today:** `SystemGrammarEngine` blindly trusts `NSSpellChecker.correction(forWordRange:)`, which is sentence-position-aware and capitalizes a word it thinks starts a sentence — producing `problemm` -> `Problem` mid-sentence (`SystemGrammarEngine.swift:42-45`). `Suggestion` has no confidence/severity/alternatives (`Models.swift:48-59`). Default `correct()` applies *every* suggestion including no-op grammar flags where `replacement == original` (`GrammarEngine.swift:22-29`). +- **GrammarGem today:** `SystemGrammarEngine` blindly trusts `NSSpellChecker.correction(forWordRange:)`, which is sentence-position-aware and capitalizes a word it thinks starts a sentence — producing `problemm` -> `Problem` mid-sentence (`SystemGrammarEngine.swift:42-45`). `Suggestion` has no confidence/severity/alternatives (`Models.swift:48-59`). Default `correct()` applies *every* suggestion including no-op grammar flags where `replacement == original` (`GrammarEngine.swift:22-29`). - **Do:** real Harper FFI; case/whitespace post-processing; add `confidence`/`severity`/`alternatives` to `Suggestion`; never auto-apply no-ops or low-confidence flags. - **Files:** `SystemGrammarEngine.swift`, `HarperEngine.swift`, `GrammarEngine.swift`, `Models.swift`, `harper-ffi/`. - **Impact: high · Effort: L** ### Mechanic 2 — The fix-as-button atomic loop (+ dismiss / ignore / add-to-dictionary) - **Grammarly:** the suggested word *is* the button (green = go/safe/done); an always-present cheap dismiss (trash/Esc) and add-to-dictionary make accepting feel safe; Tab-to-navigate / Enter-to-accept / Esc-to-dismiss removes the mouse round-trip. -- **GrammaGem today:** `FieldCard` shows a message + one `original -> replacement` + a small `Apply` button (`LiveCheckView.swift:104-119`). There is **no dismiss, no ignore, no add-to-dictionary, no keyboard accept.** Per-issue `apply()` bails silently if the field changed at all since the snapshot (`LiveMonitor.swift:82-89`), and even single-issue apply rewrites the *whole* AX value (`LiveMonitor.swift:101-103`), risking caret/undo/formatting loss. +- **GrammarGem today:** `FieldCard` shows a message + one `original -> replacement` + a small `Apply` button (`LiveCheckView.swift:104-119`). There is **no dismiss, no ignore, no add-to-dictionary, no keyboard accept.** Per-issue `apply()` bails silently if the field changed at all since the snapshot (`LiveMonitor.swift:82-89`), and even single-issue apply rewrites the *whole* AX value (`LiveMonitor.swift:101-103`), risking caret/undo/formatting loss. - **Do (realistic for a cross-app AX tool — we cannot draw a green chip inside arbitrary apps):** - Make the replacement text itself the primary tappable control in the card (the chip *is* the fix), styled by category color. - Add a per-suggestion overflow: **Dismiss** (model a `dismissedIDs: Set` on the monitor so it survives re-scans), **Ignore word** / **Add to dictionary** (route into `PersonalDictionary` -> `ignoreList`), gated by `freeDictionaryCap`. @@ -47,9 +47,9 @@ The strategic asymmetry to exploit throughout: every Grammarly value moment carr ### Mechanic 3 — Categorization, counts, and an on-device document score - **Grammarly:** four named buckets (Correctness/Clarity/Engagement/Delivery) each with a color and a count chip that doubles as a filter; one master running badge; a 0-100 percentile score that ticks up on every accept; cards ordered by severity (objective errors first) so the early wins build trust. -- **GrammaGem today:** `GrammarKind` has 6 cases but `.punctuation` is dead; the panel reduces everything to 3 dot colors (`LiveCheckView.swift:136-142`); `issueCount` is a raw sum (`LiveMonitor.swift:32`); there is **no score, no per-category chips, no severity ordering, no filter**. +- **GrammarGem today:** `GrammarKind` has 6 cases but `.punctuation` is dead; the panel reduces everything to 3 dot colors (`LiveCheckView.swift:136-142`); `issueCount` is a raw sum (`LiveMonitor.swift:32`); there is **no score, no per-category chips, no severity ordering, no filter**. - **Do (on-device, no cloud, no percentile-vs-others):** - - Introduce GrammaGem's own named buckets that fit the engine: e.g. **Correctness** (spelling/grammar/punctuation), **Clarity** (phrasing/conciseness — from the real LLM), **Polish** (repetition/capitalization/style). Use original color semantics (do NOT clone Grammarly's exact red/blue/green/purple mapping or wording). + - Introduce GrammarGem's own named buckets that fit the engine: e.g. **Correctness** (spelling/grammar/punctuation), **Clarity** (phrasing/conciseness — from the real LLM), **Polish** (repetition/capitalization/style). Use original color semantics (do NOT clone Grammarly's exact red/blue/green/purple mapping or wording). - Add **count chips per bucket** in the panel header that filter the card list; keep the menu-bar badge as the master tally (`MenuBarLabel`). - Compute a **local cleanliness score** at check-time: a deterministic function of issues-per-100-words by severity (e.g. `100 - weighted_issues`). It is honest because it's defined relative to *this document*, not a hidden user corpus. Show it on the card header and trend it on the dashboard. - **Order cards by severity then document position** (objective spelling/grammar first) so the first accept is a high-confidence win. @@ -59,18 +59,18 @@ The strategic asymmetry to exploit throughout: every Grammarly value moment carr ### Mechanic 4 — The first-run "wow" (see §4 for the full sequence) - **Grammarly:** land in an error-salted demo doc -> "we found N issues" -> one-click fixes vanish underlines + decrement counter -> goal setup -> install-everywhere prompt at the dopamine peak. -- **GrammaGem today:** `OnboardingView` only covers Accessibility + model download (`OnboardingView.swift`); it never shows the correction loop. +- **GrammarGem today:** `OnboardingView` only covers Accessibility + model download (`OnboardingView.swift`); it never shows the correction loop. - **Do:** add an in-app demo step with a pre-seeded sample, a "we found N things to fix" reveal, and a satisfying one-click-to-clean animation — *before* asking for nothing-but-praise (no install-everywhere pressure; we're already system-wide). Detail in §4. - **Files:** `OnboardingView.swift`, plus a small `DemoDocument` view reusing the highlight + apply UI. - **Impact: high · Effort: M** ### Mechanic 5 — Ongoing perceived value (privacy-native, self-vs-self) - **Grammarly:** 0-100 score, weekly Insights email ("more productive than X% of users"), lifetime words-checked counter, streaks — all cloud + behavioral-email, the exact surveillance we market against. -- **GrammaGem today:** `UsageStats` tracks `totalCorrections`, `totalAIActions`, `totalWords`, and `perDay` (`UsageStats.swift`); the dashboard shows four tiles + a 7-bar chart, but **no score, no streak, no week-over-week, no milestone, no money-saved figure**. `estimatedMinutesSaved` is a crude untuned constant (`8s/correction + 45s/AI-action`). Stats are invisible outside the main window. +- **GrammarGem today:** `UsageStats` tracks `totalCorrections`, `totalAIActions`, `totalWords`, and `perDay` (`UsageStats.swift`); the dashboard shows four tiles + a 7-bar chart, but **no score, no streak, no week-over-week, no milestone, no money-saved figure**. `estimatedMinutesSaved` is a crude untuned constant (`8s/correction + 45s/AI-action`). Stats are invisible outside the main window. - **Do (all local, no telemetry, stops nagging the moment they own it):** - **Local writing-score trend** on the dashboard (from Mechanic 3), compared only to the user's own past. - **Streak + milestones** from existing `perDay` data ("7-day streak", "1,000th correction", "100k words polished") — cheap, high word-of-mouth, and the right re-engagement hook for a no-renewal product. - - **"Subscription you didn't pay" figure** — translate usage into money: the marketing site already hammers "~\$144/yr vs pay once," but the *app* never does. Show "You've polished N times — that's the price of GrammaGem recovered M times over," computed locally. This is the strongest one-time-purchase value teaser and it lives where the user actually is. + - **"Subscription you didn't pay" figure** — translate usage into money: the marketing site already hammers "~\$144/yr vs pay once," but the *app* never does. Show "You've polished N times — that's the price of GrammarGem recovered M times over," computed locally. This is the strongest one-time-purchase value teaser and it lives where the user actually is. - **Week-over-week delta** on the bar chart and a lightweight in-app weekly recap card (NOT an email; no pipeline, no surveillance). - **Surface one stat in the menu bar dropdown** (the most-seen surface) so value reminders don't require opening the dashboard. - Tune `estimatedMinutesSaved` to scale by words changed / action type so the headline "was it worth it?" number is credible. @@ -79,7 +79,7 @@ The strategic asymmetry to exploit throughout: every Grammarly value moment carr ### Mechanic 6 — The generative "writing partner" layer (real LLM, honest gating) - **Grammarly:** sentence rewrites ("Replace with This Version" + Highlight-Changes diff), tone detection (emoji + label), tone adjustment, conciseness, GrammarlyGO — all one-click, reversible, before/after. -- **GrammaGem today:** `MLXEngine` is a stub doing `replacingOccurrences` string hacks (`MLXEngine.swift`); `tidy()` even re-introduces casing risk; `detectTone` keys off substrings. `ModelManager` downloads real weights but nothing loads them. Gated by `freeDailyAIActions = 10` shared across rewrite/tone/Ask/translate. +- **GrammarGem today:** `MLXEngine` is a stub doing `replacingOccurrences` string hacks (`MLXEngine.swift`); `tidy()` even re-introduces casing risk; `detectTone` keys off substrings. `ModelManager` downloads real weights but nothing loads them. Gated by `freeDailyAIActions = 10` shared across rewrite/tone/Ask/translate. - **Do:** - Wire the **real MLX model** from `ModelManager.modelsDirectory` (the download is currently a dead-end); uncomment `mlx-swift` in `Package.swift`. - Adopt the **one-click + reversible + before/after diff** delivery contract for every rewrite — this is what neutralizes "the AI is taking over my voice." @@ -133,7 +133,7 @@ Wire dismiss / add-to-dictionary (Mechanic 2) back into ranking so repeated fals --- -## 4. The first-run "wow" sequence for GrammaGem (original copy) +## 4. The first-run "wow" sequence for GrammarGem (original copy) Goal: a guaranteed visible win in under 60 seconds, *inside our own window* (we don't need Grammarly's blank-canvas demo trick or its install-everywhere upsell — we're already system-wide). Onboarding today (`OnboardingView.swift`) skips this entirely. @@ -145,7 +145,7 @@ Proposed flow: 3. **One satisfying click.** A single "Clean it up" button animates the highlights resolving one by one; the count ticks to 0; the paragraph visibly improves. This is the operant loop — trivial action, immediate visible reward — experienced once, on rails, with zero risk. (Reuse the apply path; it's a local string, not a live AX element, so it's completely safe.) -4. **The reveal:** "That's GrammaGem. It does this in Mail, Slack, Notes, your browser — everywhere you type." Then the *first* ask: **Accessibility** (the existing step 2 / `stepCard`), now framed as "Turn it on everywhere." +4. **The reveal:** "That's GrammarGem. It does this in Mail, Slack, Notes, your browser — everywhere you type." Then the *first* ask: **Accessibility** (the existing step 2 / `stepCard`), now framed as "Turn it on everywhere." 5. **Optional model step:** keep the existing "Download the on-device model (optional) — grammar works instantly with no download" card. Frame the AI features as a bonus, not a blocker. diff --git a/harper-ffi/README.md b/harper-ffi/README.md index 2d4153f..6fd2d49 100644 --- a/harper-ffi/README.md +++ b/harper-ffi/README.md @@ -5,7 +5,7 @@ A thin Rust static library that exposes the **Harper** grammar core integration (spec §1, §2, §8). The Swift side talks to this through the `GrammarEngine` protocol; today -[`HarperEngine.swift`](../Sources/GrammaGem/Grammar/HarperEngine.swift) ships a +[`HarperEngine.swift`](../Sources/GrammarGem/Grammar/HarperEngine.swift) ships a small pure-Swift rule stub so the system loop + tests run with zero native deps. ## Planned shape diff --git a/scripts/build-metallib.sh b/scripts/build-metallib.sh index ce22774..755a7b4 100755 --- a/scripts/build-metallib.sh +++ b/scripts/build-metallib.sh @@ -15,7 +15,7 @@ cd "$ROOT" DD=".xcode-build" echo "==> xcodebuild (compiles mlx-swift metal kernels — a few minutes)" -xcodebuild build -scheme GrammaGem -configuration Release \ +xcodebuild build -scheme GrammarGem -configuration Release \ -destination 'platform=macOS,arch=arm64' \ -derivedDataPath "$DD" -skipMacroValidation >/dev/null diff --git a/scripts/build.sh b/scripts/build.sh index e530154..1d66509 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Build a runnable GrammaGem.app bundle (arm64 / Apple Silicon — the MLX +# Build a runnable GrammarGem.app bundle (arm64 / Apple Silicon — the MLX # on-device LLM runtime is Metal-only, so Intel is not supported). # Usage: ./scripts/build.sh (run from the mac/ directory) set -euo pipefail @@ -8,7 +8,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" CONFIG="release" -APP="GrammaGem.app" +APP="GrammarGem.app" DIST="dist" echo "==> Building Harper static lib (universal)" @@ -24,7 +24,7 @@ echo "==> Assembling ${APP}" rm -rf "${DIST}/${APP}" mkdir -p "${DIST}/${APP}/Contents/MacOS" "${DIST}/${APP}/Contents/Resources" -cp "${BIN}/GrammaGem" "${DIST}/${APP}/Contents/MacOS/GrammaGem" +cp "${BIN}/GrammarGem" "${DIST}/${APP}/Contents/MacOS/GrammarGem" cp "AppSupport/Info.plist" "${DIST}/${APP}/Contents/Info.plist" cp "AppSupport/AppIcon.icns" "${DIST}/${APP}/Contents/Resources/AppIcon.icns" @@ -52,8 +52,8 @@ SPARKLE_FW="$(find .build -path '*Sparkle.xcframework/macos-arm64*/Sparkle.frame mkdir -p "${DIST}/${APP}/Contents/Frameworks" cp -R "${SPARKLE_FW}" "${DIST}/${APP}/Contents/Frameworks/" install_name_tool -add_rpath "@executable_path/../Frameworks" \ - "${DIST}/${APP}/Contents/MacOS/GrammaGem" 2>/dev/null || true + "${DIST}/${APP}/Contents/MacOS/GrammarGem" 2>/dev/null || true echo "==> Built ${DIST}/${APP}" -lipo -info "${DIST}/${APP}/Contents/MacOS/GrammaGem" +lipo -info "${DIST}/${APP}/Contents/MacOS/GrammarGem" echo " Run with: open \"${DIST}/${APP}\" (grant Accessibility on first launch)" diff --git a/scripts/make-icon.swift b/scripts/make-icon.swift index 81c0e3c..3226134 100644 --- a/scripts/make-icon.swift +++ b/scripts/make-icon.swift @@ -1,5 +1,5 @@ #!/usr/bin/env swift -// Generates AppIcon.iconset (PNGs) for GrammaGem — a gold pen-nib on the ink +// Generates AppIcon.iconset (PNGs) for GrammarGem — a gold pen-nib on the ink // brand square. Run from mac/: swift scripts/make-icon.swift // Then: iconutil -c icns AppIcon.iconset -o AppSupport/AppIcon.icns import AppKit diff --git a/scripts/release.sh b/scripts/release.sh index 5699081..6711020 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# Cut a GrammaGem release and publish the Sparkle appcast (hosted on grammagem.app). +# Cut a GrammarGem release and publish the Sparkle appcast (hosted on grammargem.com). # # ./scripts/release.sh e.g. ./scripts/release.sh 0.2.0 # # Steps: stamp version -> build -> sign inside-out (incl. Sparkle) -> notarize + # staple -> zip -> generate the EdDSA-signed appcast.xml. Then upload the .zip and -# appcast.xml to grammagem.app so every installed app shows "Update available". +# appcast.xml to grammargem.com so every installed app shows "Update available". # # Notarization needs ONE of: # NOTARY_PROFILE (xcrun notarytool store-credentials) @@ -17,10 +17,10 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" VERSION="${1:?usage: release.sh e.g. release.sh 0.2.0}" -APP="dist/GrammaGem.app" +APP="dist/GrammarGem.app" RELDIR="dist/releases" -ZIP="${RELDIR}/GrammaGem-${VERSION}.zip" -DL_PREFIX="${DL_PREFIX:-https://grammagem.app/releases/}" +ZIP="${RELDIR}/GrammarGem-${VERSION}.zip" +DL_PREFIX="${DL_PREFIX:-https://www.grammargem.com/releases/}" PB=/usr/libexec/PlistBuddy # 1. Stamp the marketing version + bump the monotonic build number. @@ -61,11 +61,11 @@ GA="$(find .build/artifacts -name generate_appcast -type f | head -1)" "$GA" --download-url-prefix "$DL_PREFIX" "$RELDIR" echo -echo "✓ Release ${VERSION} built. Upload these to grammagem.app:" +echo "✓ Release ${VERSION} built. Upload these to grammargem.com:" echo " ${ZIP}" -echo " -> ${DL_PREFIX}GrammaGem-${VERSION}.zip" +echo " -> ${DL_PREFIX}GrammarGem-${VERSION}.zip" echo " ${RELDIR}/appcast.xml" -echo " -> https://grammagem.app/appcast.xml (matches Info.plist SUFeedURL)" +echo " -> https://www.grammargem.com/appcast.xml (matches Info.plist SUFeedURL)" echo echo " Once uploaded, installed apps show an 'Update available' button within a day" echo " (or immediately via menu bar -> Check for Updates...)." diff --git a/scripts/sign-app.sh b/scripts/sign-app.sh index 869f88a..743b882 100755 --- a/scripts/sign-app.sh +++ b/scripts/sign-app.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash -# Code-sign GrammaGem.app with Developer ID + Hardened Runtime, signing the +# Code-sign GrammarGem.app with Developer ID + Hardened Runtime, signing the # embedded Sparkle.framework's nested helpers INSIDE-OUT first (required for a # valid, notarizable signature). # -# Usage: ./scripts/sign-app.sh [path/to/GrammaGem.app] +# Usage: ./scripts/sign-app.sh [path/to/GrammarGem.app] set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" DEV_ID="${DEV_ID:-Developer ID Application: Marc Schultheiss (D5KT5B9Z9M)}" -ENTITLEMENTS="AppSupport/GrammaGem.entitlements" -APP="${1:-dist/GrammaGem.app}" +ENTITLEMENTS="AppSupport/GrammarGem.entitlements" +APP="${1:-dist/GrammarGem.app}" sign() { codesign --force --timestamp --options runtime --sign "$DEV_ID" "$@"; } diff --git a/scripts/sign-notarize.sh b/scripts/sign-notarize.sh index a5d58cc..38787db 100755 --- a/scripts/sign-notarize.sh +++ b/scripts/sign-notarize.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Build (universal), codesign (Hardened Runtime + Developer ID), notarize, and -# staple GrammaGem.app. Requires the Apple Developer Program ($99/yr) — a FIXED +# staple GrammarGem.app. Requires the Apple Developer Program ($99/yr) — a FIXED # cost, not per-user. # # Credentials are read from the environment or a stored keychain profile — never @@ -15,9 +15,9 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" DEV_ID="${DEV_ID:-Developer ID Application: Marc Schultheiss (D5KT5B9Z9M)}" -APP="dist/GrammaGem.app" -ZIP="dist/GrammaGem.zip" -ENTITLEMENTS="AppSupport/GrammaGem.entitlements" +APP="dist/GrammarGem.app" +ZIP="dist/GrammarGem.zip" +ENTITLEMENTS="AppSupport/GrammarGem.entitlements" echo "==> Building universal bundle" ./scripts/build.sh >/dev/null