mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
M0a fixup: unignore local Packages/, add missing files, make Linux CI pass
The initial M0a commit was incomplete: .gitignore's `Packages/` rule
(meant for the legacy pre-Xcode-14 SwiftPM checkout dir) silently
swallowed three new files that SHOULD have been committed:
- scarf/Packages/ScarfCore/Package.swift
- scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConstants.swift
- scarf/Packages/ScarfCore/Tests/ScarfCoreTests/ScarfCoreSmokeTests.swift
The 12 moved models slipped through because `git mv` preserves tracking
across gitignored destinations, but new files in that tree did not.
Fix: add `!scarf/Packages/` override so our local SPM package is always
tracked; keep the top-level `Packages/` ignore for the historical case.
Also verified M0a builds + tests green on Linux via
`docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work swift:6.0 swift test`.
To make that work, two small, Apple-platform-preserving guards:
- `sqliteTransient` in HermesConstants.swift wrapped in
`#if canImport(SQLite3)` — SQLite3 is not a system module on Linux
swift-corelibs-foundation. Apple builds compile unchanged.
- `ToolKind.displayName` and `MCPTransport.displayName` wrapped in
`#if canImport(Darwin)` — `LocalizedStringResource` is Apple-only.
Apple builds compile unchanged.
Additionally:
- Package.swift pinned to Swift 5 language mode, matching the Mac app's
`SWIFT_VERSION = 5.0`. Two types (`ACPEvent.availableCommands` and
`ACPToolCallEvent.rawInput`) claim `Sendable` while carrying
`[String: Any]` — strict Swift 6 rejects that. Comment in Package.swift
flags this for a future typed-payloads cleanup + bump to `.v6`.
- ScarfCoreSmokeTests now contains 16 tests exercising every M0a
`public init` so parameter drift fails CI instead of a reviewer.
- IOS_PORT_PLAN.md updated with what actually shipped, the Linux-CI
guards + patterns future phases should reuse, and the Sendable
follow-up flagged under "Rules next phases can rely on".
Test results (Linux, Swift 6.0.3):
Suite M0aPublicInitTests: 15 tests passed
Suite ScarfCoreSmokeTests: 1 test passed
Total: 16 / 16 passed
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// swift-tools-version: 6.0
|
||||
// Platform-neutral core for the Scarf app family (macOS and iOS).
|
||||
//
|
||||
// `ScarfCore` holds types that do not depend on AppKit, UIKit, or any
|
||||
// platform-specific system service. The macOS and iOS app targets each link
|
||||
// this package and provide their own platform shells (Sparkle + SwiftTerm on
|
||||
// macOS; Citadel-based SSH transport on iOS).
|
||||
//
|
||||
// Minimums are chosen to match the Mac app (macOS 14.6) and the locked
|
||||
// v1 iOS decision (iOS 18). Raising iOS later is free; lowering is not —
|
||||
// the ViewModels on `@Observable` / `NavigationStack` are iOS 17+ features
|
||||
// and we standardize on iOS 18 for feature parity with the Mac codebase.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ScarfCore",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.macOS(.v14),
|
||||
.iOS(.v18),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "ScarfCore",
|
||||
targets: ["ScarfCore"]
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "ScarfCore",
|
||||
path: "Sources/ScarfCore",
|
||||
swiftSettings: [
|
||||
// Swift 5 language mode mirrors the Mac app target's
|
||||
// `SWIFT_VERSION = 5.0` build setting. Moving to strict
|
||||
// Swift 6 concurrency is a real refactor — several types
|
||||
// (`ACPEvent.availableCommands` carrying `[[String: Any]]`,
|
||||
// `ACPToolCallEvent.rawInput: [String: Any]?`) claim
|
||||
// `Sendable` without being strictly-Sendable. A follow-up
|
||||
// phase will replace those with typed payloads, then this
|
||||
// setting can bump to `.v6`.
|
||||
.swiftLanguageMode(.v5),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ScarfCoreTests",
|
||||
dependencies: ["ScarfCore"],
|
||||
path: "Tests/ScarfCoreTests"
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
#if canImport(SQLite3)
|
||||
import SQLite3
|
||||
#endif
|
||||
|
||||
// MARK: - SQLite Constants
|
||||
|
||||
#if canImport(SQLite3)
|
||||
/// SQLITE_TRANSIENT tells SQLite to make its own copy of bound string data.
|
||||
/// The C macro is defined as ((sqlite3_destructor_type)-1) which can't be imported directly into Swift.
|
||||
///
|
||||
/// Gated behind `canImport(SQLite3)` so this file compiles on Linux (where
|
||||
/// SPM has no built-in `SQLite3` system module). Apple platforms — the only
|
||||
/// runtime targets that actually execute this code — compile it unchanged.
|
||||
public nonisolated let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
||||
#endif
|
||||
|
||||
// MARK: - Query Defaults
|
||||
|
||||
public enum QueryDefaults: Sendable {
|
||||
public nonisolated static let sessionLimit = 100
|
||||
public nonisolated static let messageSearchLimit = 50
|
||||
public nonisolated static let toolCallLimit = 50
|
||||
public nonisolated static let sessionPreviewLimit = 10
|
||||
public nonisolated static let previewContentLength = 100
|
||||
public nonisolated static let logLineLimit = 200
|
||||
public nonisolated static let defaultSilenceThreshold = 200
|
||||
}
|
||||
|
||||
// MARK: - File Size Formatting
|
||||
|
||||
public enum FileSizeUnit: Sendable {
|
||||
public nonisolated static let kilobyte = 1_024.0
|
||||
public nonisolated static let megabyte = 1_048_576.0
|
||||
}
|
||||
@@ -6,12 +6,14 @@ public enum MCPTransport: String, Sendable, Equatable, CaseIterable, Identifiabl
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
#if canImport(Darwin)
|
||||
public var displayName: LocalizedStringResource {
|
||||
switch self {
|
||||
case .stdio: return "Local (stdio)"
|
||||
case .http: return "Remote (HTTP)"
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public struct HermesMCPServer: Identifiable, Sendable, Equatable {
|
||||
|
||||
@@ -125,6 +125,7 @@ public enum ToolKind: String, Sendable, CaseIterable {
|
||||
case browser
|
||||
case other
|
||||
|
||||
#if canImport(Darwin)
|
||||
public var displayName: LocalizedStringResource {
|
||||
switch self {
|
||||
case .read: return "Read"
|
||||
@@ -135,6 +136,7 @@ public enum ToolKind: String, Sendable, CaseIterable {
|
||||
case .other: return "Other"
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public var icon: String {
|
||||
switch self {
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import ScarfCore
|
||||
|
||||
/// Smoke test — catches "does the package link?"-class regressions.
|
||||
@Suite struct ScarfCoreSmokeTests {
|
||||
@Test func packageLinks() {
|
||||
// If this compiles and runs, ScarfCore loaded.
|
||||
}
|
||||
}
|
||||
|
||||
/// Exercises every `public init` generated in M0a. If any memberwise init's
|
||||
/// parameter list drifted away from the stored properties (wrong order, wrong
|
||||
/// type, missing field), the compiler fails here — the whole point of these
|
||||
/// tests is to give the Linux CI something to catch that before a reviewer
|
||||
/// has to build on a Mac.
|
||||
@Suite struct M0aPublicInitTests {
|
||||
@Test func hermesSessionInitAndDerivations() {
|
||||
let s = HermesSession(
|
||||
id: "s1",
|
||||
source: "cli",
|
||||
userId: "u",
|
||||
model: "gpt-4",
|
||||
title: "Hello",
|
||||
parentSessionId: nil,
|
||||
startedAt: Date(timeIntervalSince1970: 0),
|
||||
endedAt: Date(timeIntervalSince1970: 60),
|
||||
endReason: nil,
|
||||
messageCount: 3,
|
||||
toolCallCount: 1,
|
||||
inputTokens: 100,
|
||||
outputTokens: 200,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
estimatedCostUSD: 0.01,
|
||||
reasoningTokens: 50,
|
||||
actualCostUSD: nil,
|
||||
costStatus: nil,
|
||||
billingProvider: nil
|
||||
)
|
||||
#expect(s.displayTitle == "Hello")
|
||||
#expect(s.totalTokens == 350)
|
||||
#expect(s.duration == 60)
|
||||
#expect(s.isSubagent == false)
|
||||
#expect(s.costIsActual == false)
|
||||
#expect(s.displayCostUSD == 0.01)
|
||||
// Subagent variant
|
||||
let child = s.withTitle("Child")
|
||||
#expect(child.displayTitle == "Child")
|
||||
}
|
||||
|
||||
@Test func hermesMessageInitAndRolePredicates() {
|
||||
let user = HermesMessage(
|
||||
id: 1, sessionId: "s", role: "user", content: "hi",
|
||||
toolCallId: nil, toolCalls: [], toolName: nil,
|
||||
timestamp: nil, tokenCount: nil, finishReason: nil, reasoning: nil
|
||||
)
|
||||
#expect(user.isUser && !user.isAssistant && !user.isToolResult)
|
||||
|
||||
let asst = HermesMessage(
|
||||
id: 2, sessionId: "s", role: "assistant", content: "hello",
|
||||
toolCallId: nil, toolCalls: [], toolName: nil,
|
||||
timestamp: nil, tokenCount: nil, finishReason: nil,
|
||||
reasoning: "thinking..."
|
||||
)
|
||||
#expect(asst.isAssistant && asst.hasReasoning)
|
||||
}
|
||||
|
||||
@Test func hermesToolCallExplicitInit() {
|
||||
let call = HermesToolCall(callId: "c1", functionName: "read_file", arguments: "{\"path\":\"/tmp\"}")
|
||||
#expect(call.id == "c1")
|
||||
#expect(call.toolKind == .read)
|
||||
#expect(call.argumentsSummary == "/tmp")
|
||||
}
|
||||
|
||||
@Test func hermesConfigEmptyAndMemberwise() {
|
||||
// `.empty` exercises every nested init internally — if any nested
|
||||
// settings struct's init drifted, HermesConfig.empty would fail to
|
||||
// compile. Importing and touching .empty proves the chain works.
|
||||
let c = HermesConfig.empty
|
||||
#expect(c.model == "unknown")
|
||||
#expect(c.display.skin == "default")
|
||||
#expect(c.terminal.cwd == ".")
|
||||
#expect(c.browser.inactivityTimeout == 120)
|
||||
#expect(c.security.redactSecrets == true)
|
||||
#expect(c.humanDelay.mode == "off")
|
||||
#expect(c.compression.enabled == true)
|
||||
#expect(c.checkpoints.enabled == true)
|
||||
#expect(c.logging.level == "INFO")
|
||||
#expect(c.discord.requireMention == true)
|
||||
#expect(c.telegram.reactions == false)
|
||||
#expect(c.slack.replyToMode == "first")
|
||||
#expect(c.matrix.autoThread == true)
|
||||
#expect(c.mattermost.replyMode == "off")
|
||||
#expect(c.whatsapp.unauthorizedDMBehavior == "pair")
|
||||
#expect(c.homeAssistant.cooldownSeconds == 30)
|
||||
#expect(c.auxiliary.vision.provider == "auto")
|
||||
}
|
||||
|
||||
@Test func hermesCronJobCodableRoundTrip() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": "job1",
|
||||
"name": "Daily Summary",
|
||||
"prompt": "summarize yesterday",
|
||||
"skills": ["email"],
|
||||
"model": null,
|
||||
"schedule": { "kind": "daily", "run_at": "09:00", "display": "Every day 9am", "expression": null },
|
||||
"enabled": true,
|
||||
"state": "scheduled",
|
||||
"deliver": "discord:general:chat-chan",
|
||||
"next_run_at": "2026-04-23T09:00:00Z",
|
||||
"last_run_at": null,
|
||||
"last_error": null,
|
||||
"pre_run_script": null,
|
||||
"delivery_failures": 0,
|
||||
"last_delivery_error": null,
|
||||
"timeout_type": "soft",
|
||||
"timeout_seconds": 300,
|
||||
"silent": false
|
||||
}
|
||||
"""
|
||||
let job = try JSONDecoder().decode(HermesCronJob.self, from: Data(json.utf8))
|
||||
#expect(job.id == "job1")
|
||||
#expect(job.stateIcon == "clock")
|
||||
#expect(job.deliveryDisplay == "Discord thread chat-chan in general")
|
||||
#expect(job.schedule.kind == "daily")
|
||||
#expect(job.silent == false)
|
||||
|
||||
// Re-encode and decode again to confirm encoder output is valid.
|
||||
let encoded = try JSONEncoder().encode(job)
|
||||
let roundTripped = try JSONDecoder().decode(HermesCronJob.self, from: encoded)
|
||||
#expect(roundTripped.id == job.id)
|
||||
}
|
||||
|
||||
@Test func hermesMCPServerInit() {
|
||||
let server = HermesMCPServer(
|
||||
name: "gh", transport: .stdio, command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-github"],
|
||||
url: nil, auth: nil,
|
||||
env: ["GITHUB_TOKEN": "x"], headers: [:],
|
||||
timeout: 30, connectTimeout: 5, enabled: true,
|
||||
toolsInclude: [], toolsExclude: [],
|
||||
resourcesEnabled: true, promptsEnabled: true, hasOAuthToken: false
|
||||
)
|
||||
#expect(server.id == "gh")
|
||||
#expect(server.summary == "npx -y @modelcontextprotocol/server-github")
|
||||
|
||||
let http = HermesMCPServer(
|
||||
name: "linear", transport: .http, command: nil, args: [],
|
||||
url: "https://mcp.linear.app/sse", auth: "oauth",
|
||||
env: [:], headers: [:], timeout: nil, connectTimeout: nil,
|
||||
enabled: true, toolsInclude: [], toolsExclude: [],
|
||||
resourcesEnabled: true, promptsEnabled: true, hasOAuthToken: true
|
||||
)
|
||||
#expect(http.summary == "https://mcp.linear.app/sse")
|
||||
}
|
||||
|
||||
@Test func mcpServerPresetGalleryReadable() {
|
||||
#expect(!MCPServerPreset.gallery.isEmpty)
|
||||
#expect(MCPServerPreset.gallery.contains { $0.id == "filesystem" })
|
||||
// Every preset in the gallery should have a docsURL.
|
||||
for p in MCPServerPreset.gallery {
|
||||
#expect(!p.docsURL.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func hermesPathSetDerivations() {
|
||||
let local = HermesPathSet(home: "/Users/alan/.hermes", isRemote: false, binaryHint: nil)
|
||||
#expect(local.stateDB == "/Users/alan/.hermes/state.db")
|
||||
#expect(local.memoryMD == "/Users/alan/.hermes/memories/MEMORY.md")
|
||||
#expect(local.userMD == "/Users/alan/.hermes/memories/USER.md")
|
||||
#expect(local.projectsRegistry == "/Users/alan/.hermes/scarf/projects.json")
|
||||
// hermesBinary on local looks up real fs — we can only guarantee it
|
||||
// returns one of the candidates (or the fallback).
|
||||
#expect(HermesPathSet.hermesBinaryCandidates.contains(local.hermesBinary)
|
||||
|| local.hermesBinary == HermesPathSet.hermesBinaryCandidates[0])
|
||||
|
||||
let remote = HermesPathSet(home: "~/.hermes", isRemote: true, binaryHint: "/usr/local/bin/hermes")
|
||||
#expect(remote.hermesBinary == "/usr/local/bin/hermes")
|
||||
let remoteNoHint = HermesPathSet(home: "~/.hermes", isRemote: true, binaryHint: nil)
|
||||
#expect(remoteNoHint.hermesBinary == "hermes")
|
||||
}
|
||||
|
||||
@Test func hermesSkillInit() {
|
||||
let skill = HermesSkill(
|
||||
id: "email.send", name: "send email", category: "Email",
|
||||
path: "/a/b", files: ["send.py"], requiredConfig: ["SMTP_HOST"]
|
||||
)
|
||||
let cat = HermesSkillCategory(id: "email", name: "Email", skills: [skill])
|
||||
#expect(cat.skills.first?.id == "email.send")
|
||||
}
|
||||
|
||||
@Test func hermesSlashCommandInit() {
|
||||
let acp = HermesSlashCommand(name: "/clear", description: "Clear context", argumentHint: nil, source: .acp)
|
||||
let quick = HermesSlashCommand(name: "/brief", description: "Summary", argumentHint: "topic", source: .quickCommand)
|
||||
#expect(acp.source == .acp)
|
||||
#expect(quick.source == .quickCommand)
|
||||
#expect(acp.id == "/clear")
|
||||
}
|
||||
|
||||
@Test func hermesToolInitAndKnownPlatformIcon() {
|
||||
let ts = HermesToolset(name: "browser", description: "Web", icon: "safari", enabled: true)
|
||||
#expect(ts.id == "browser")
|
||||
let plat = HermesToolPlatform(name: "cli", displayName: "CLI", icon: "terminal")
|
||||
#expect(plat.id == "cli")
|
||||
|
||||
// KnownPlatforms lookup — guards that the icon-map path didn't break.
|
||||
#expect(KnownPlatforms.icon(for: "discord") == "bubble.left.and.bubble.right")
|
||||
#expect(KnownPlatforms.icon(for: "unknown") == "bubble.left")
|
||||
#expect(KnownPlatforms.all.count >= 13)
|
||||
}
|
||||
|
||||
@Test func acpRequestAndEvents() throws {
|
||||
let req = ACPRequest(id: 1, method: "session/new", params: ["foo": AnyCodable("bar")])
|
||||
let data = try JSONEncoder().encode(req)
|
||||
let decoded = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
#expect(decoded?["method"] as? String == "session/new")
|
||||
#expect(decoded?["jsonrpc"] as? String == "2.0")
|
||||
|
||||
let evt = ACPToolCallEvent(
|
||||
toolCallId: "t1", title: "read_file: /tmp", kind: "read",
|
||||
status: "pending", content: "", rawInput: ["path": "/tmp"]
|
||||
)
|
||||
#expect(evt.functionName == "read_file")
|
||||
#expect(evt.argumentsSummary == "/tmp")
|
||||
|
||||
let upd = ACPToolCallUpdateEvent(
|
||||
toolCallId: "t1", kind: "read", status: "completed",
|
||||
content: "hello", rawOutput: nil
|
||||
)
|
||||
#expect(upd.status == "completed")
|
||||
|
||||
let perm = ACPPermissionRequestEvent(
|
||||
toolCallTitle: "write_file: /etc/passwd", toolCallKind: "edit",
|
||||
options: [(optionId: "allow_once", name: "Allow once")]
|
||||
)
|
||||
#expect(perm.options.first?.optionId == "allow_once")
|
||||
|
||||
let prompt = ACPPromptResult(
|
||||
stopReason: "end_turn", inputTokens: 100, outputTokens: 50,
|
||||
thoughtTokens: 20, cachedReadTokens: 10
|
||||
)
|
||||
#expect(prompt.stopReason == "end_turn")
|
||||
}
|
||||
|
||||
@Test func projectDashboardInitChain() {
|
||||
let point = ChartDataPoint(x: "Mon", y: 3)
|
||||
let series = ChartSeries(name: "Calls", color: "blue", data: [point])
|
||||
let item = ListItem(text: "task 1", status: "done")
|
||||
let widget = DashboardWidget(
|
||||
type: "chart", title: "Calls per day",
|
||||
value: .number(12), icon: nil, color: nil, subtitle: nil,
|
||||
label: nil, content: nil, format: nil,
|
||||
columns: nil, rows: nil,
|
||||
chartType: "line", xLabel: "day", yLabel: "count",
|
||||
series: [series], items: [item],
|
||||
url: nil, height: nil
|
||||
)
|
||||
#expect(widget.id == "chart:Calls per day")
|
||||
#expect(widget.value?.displayString == "12")
|
||||
|
||||
let theme = DashboardTheme(accent: "blue")
|
||||
let section = DashboardSection(title: "Main", columns: 2, widgets: [widget])
|
||||
let dash = ProjectDashboard(
|
||||
version: 1, title: "Demo", description: nil,
|
||||
updatedAt: "2026-01-01", theme: theme, sections: [section]
|
||||
)
|
||||
#expect(dash.sections.first?.columnCount == 2)
|
||||
|
||||
let entry = ProjectEntry(name: "demo", path: "/a/b/demo")
|
||||
#expect(entry.dashboardPath == "/a/b/demo/.scarf/dashboard.json")
|
||||
|
||||
let reg = ProjectRegistry(projects: [entry])
|
||||
#expect(reg.projects.first?.id == "demo")
|
||||
}
|
||||
|
||||
@Test func widgetValueCodable() throws {
|
||||
let a = try JSONDecoder().decode(WidgetValue.self, from: Data("42".utf8))
|
||||
#expect(a == .number(42))
|
||||
#expect(a.displayString == "42")
|
||||
|
||||
let b = try JSONDecoder().decode(WidgetValue.self, from: Data("\"hi\"".utf8))
|
||||
#expect(b == .string("hi"))
|
||||
|
||||
// Fraction formatting path
|
||||
let c = WidgetValue.number(1.5)
|
||||
#expect(c.displayString.contains("1.5") || c.displayString.contains("1,5"))
|
||||
}
|
||||
|
||||
@Test func queryDefaultsAndFileSizeUnit() {
|
||||
#expect(QueryDefaults.sessionLimit == 100)
|
||||
#expect(FileSizeUnit.kilobyte == 1_024.0)
|
||||
}
|
||||
}
|
||||
+61
-17
@@ -160,25 +160,69 @@ the Mac app in a working state:
|
||||
|
||||
## Progress Log
|
||||
|
||||
### M0a — in progress
|
||||
**Goal:** Create `Packages/ScarfCore` scaffolding and migrate the 13 leaf
|
||||
Model files to it. `ServerContext.swift` stays in the Mac target (it
|
||||
depends on Transport + HermesFileService and is not a leaf).
|
||||
### M0a — shipped in PR #31
|
||||
|
||||
Expected artifacts when done:
|
||||
- `Packages/ScarfCore/Package.swift` exists.
|
||||
- 13 model files moved under `Sources/ScarfCore/Models/`.
|
||||
- `HermesConstants.swift` split: portable parts (`sqliteTransient`,
|
||||
`QueryDefaults`, `FileSizeUnit`) move to ScarfCore; the deprecated
|
||||
`HermesPaths` enum stays behind (it references `ServerContext.local`).
|
||||
- Every moved type annotated `public` with explicit `public init`.
|
||||
- `scarf.xcodeproj/project.pbxproj` gains one `XCLocalSwiftPackageReference`
|
||||
for `Packages/ScarfCore` and one `XCSwiftPackageProductDependency` on the
|
||||
`scarf` target (and `scarfTests` + `scarfUITests` inherit via the target).
|
||||
- 35 main-target files get `import ScarfCore` added.
|
||||
- Mac app builds and runs identically to before.
|
||||
**Shipped:**
|
||||
|
||||
(This section will be finalized when M0a is merged.)
|
||||
- `Packages/ScarfCore/Package.swift` (Swift tools 6.0, targets macOS 14 +
|
||||
iOS 18). **Language mode pinned at `.v5`** to match the Mac app's
|
||||
`SWIFT_VERSION = 5.0`. Two types (`ACPEvent.availableCommands` and
|
||||
`ACPToolCallEvent.rawInput`) claim `Sendable` while carrying
|
||||
`[String: Any]` payloads — strict Swift 6 rejects that. A future
|
||||
cleanup phase should replace those with typed payloads and bump to
|
||||
`.v6`.
|
||||
- 13 leaf model files moved under `Sources/ScarfCore/Models/`.
|
||||
- `HermesConstants.swift` split: `sqliteTransient` + `QueryDefaults` +
|
||||
`FileSizeUnit` are in ScarfCore; the deprecated `HermesPaths` enum is
|
||||
parked in the Mac target at `HermesPaths+Deprecated.swift`. Zero
|
||||
callers in-tree — it can be deleted in M0b alongside `ServerContext`.
|
||||
- Every moved type, member, and (where needed) nested `CodingKeys` is
|
||||
`public`. Every struct got an explicit `public init(...)` — Swift's
|
||||
synthesized memberwise init is `internal` and would have broken
|
||||
cross-module construction. A throwaway Python generator did the
|
||||
mechanical work; tests in `ScarfCoreTests` exercise every generated
|
||||
init so parameter drift would fail CI, not a reviewer.
|
||||
- `scarf.xcodeproj/project.pbxproj` gains one
|
||||
`XCLocalSwiftPackageReference` for `Packages/ScarfCore` and links the
|
||||
product into the `scarf` target.
|
||||
- 49 main-target files (not 35 as originally estimated — many `View`
|
||||
files only `import SwiftUI` without `Foundation`) got
|
||||
`import ScarfCore`.
|
||||
|
||||
**Linux-CI compatibility additions (for `swift test` in containers):**
|
||||
|
||||
- `SQLite3` system module exists on macOS/iOS but not on Linux
|
||||
swift-corelibs. `sqliteTransient` in `HermesConstants.swift` is
|
||||
wrapped in `#if canImport(SQLite3)`. Apple platforms compile it
|
||||
unchanged; Linux just doesn't see it (no one on Linux will execute
|
||||
Hermes DB code anyway).
|
||||
- `LocalizedStringResource` is an Apple-only Foundation type.
|
||||
`ToolKind.displayName` (in `HermesMessage.swift`) and
|
||||
`MCPTransport.displayName` (in `HermesMCPServer.swift`) are wrapped
|
||||
in `#if canImport(Darwin)`. Apple platforms compile them unchanged;
|
||||
Linux builds skip them.
|
||||
|
||||
**Test coverage (`ScarfCoreTests`):** 16 tests that construct every
|
||||
moved type via its `public init`, verify computed properties, round-trip
|
||||
Codable (`HermesCronJob`, `WidgetValue`), exercise nested config
|
||||
`.empty` chains, and assert `KnownPlatforms` / `MCPServerPreset.gallery`
|
||||
statics are readable. Run via `docker run --rm -v
|
||||
$PWD/scarf/Packages/ScarfCore:/work -w /work swift:6.0 swift test`.
|
||||
|
||||
**Rules next phases can rely on:**
|
||||
|
||||
- The `public init` pattern is now established for ScarfCore structs.
|
||||
M0b+ should add explicit `public init(...)` to every new struct moved
|
||||
into the package.
|
||||
- `#if canImport(Darwin)` is the package's "Apple-only API" guard.
|
||||
Prefer this over `os(iOS) || os(macOS) || ...` — it's shorter and
|
||||
catches the same platforms.
|
||||
- `#if canImport(SQLite3)` is the pattern for anything that needs
|
||||
Apple's built-in SQLite. When HermesDataService moves in M0c, use
|
||||
this same guard for the actual Swift-SQLite bindings.
|
||||
- The Mac app still uses Swift 5 language mode. Do **not** add
|
||||
`nonisolated` to new ScarfCore APIs pre-emptively; match the
|
||||
surrounding conventions.
|
||||
|
||||
### M0b — pending
|
||||
### M0c — pending
|
||||
|
||||
Reference in New Issue
Block a user