iOS port M0c: extract portable Services to ScarfCore

Third of four M0 sub-PRs. Moves the four Services that have no dependency
on Mac-target code or AppKit into ScarfCore, so the Mac + (future) iOS
targets can share them.

Files moved (4):
  scarf/Core/Services/HermesDataService.swift  (658 lines, SQLite reader + SnapshotCoordinator actor)
  scarf/Core/Services/HermesLogService.swift   (log tail + parse, LogEntry + LogLevel)
  scarf/Core/Services/ModelCatalogService.swift (models.dev JSON reader, HermesModelInfo + HermesProviderInfo)
  scarf/Core/Services/ProjectDashboardService.swift (per-project dashboard I/O)

Not moved, with reason:
  HermesFileService.swift  — carries the big shell-enrichment logic; a
    later phase can port once iOS has a clearer env story for ACP spawns.
  HermesEnvService.swift   — depends on HermesFileService.
  HermesFileWatcher.swift  — depends on HermesFileService.
  ACPClient.swift          — M1's job (the ACPChannel refactor).
  UpdaterService.swift     — wraps Sparkle, stays Mac-only forever.

Platform guards:
  HermesDataService.swift is wrapped in `#if canImport(SQLite3) ... #endif`
  for the whole file. SQLite3 isn't a system module on Linux
  swift-corelibs-foundation. Apple platforms compile unchanged. Linux
  builds skip the file entirely; nothing in ScarfCore references
  HermesDataService from outside the file, so there's no downstream
  fallout.

  ModelCatalogService `import os` / Logger definition / call site all
  guarded with `#if canImport(os)`. Linux gets silent logging.

  HermesLogService + ProjectDashboardService use only Foundation —
  no guards needed.

Other fixes:
  - Features/Settings/Views/Components/ModelPickerSheet.swift (the one
    remaining consumer) gains `import ScarfCore`.
  - Self-referential `import ScarfCore` stripped from each moved file.

Test coverage: 8 new tests in ScarfCoreTests/M0cServicesTests.swift:
  - HermesLogService.parseLine exercised via readLastLines on a real
    tmp file with three formats — v0.9.0+ with session tag, older
    without, and garbage fallback. Pins CLAUDE.md's optional-session-tag
    invariant.
  - LogLevel SwiftUI colour strings pinned.
  - HermesModelInfo.contextDisplay across 1M / 200K / 500 / nil cases;
    costDisplay with and without costs.
  - ModelCatalogService load path end-to-end against a synthetic
    models_dev_cache.json lookalike — providers sorted, models
    filtered, provider(for:) resolves both full-scan and slash-prefixed
    IDs.
  - Malformed + missing catalog files return empty, no crash.
  - ProjectDashboardService round-trips ProjectRegistry + reads a
    synthetic .scarf/dashboard.json.

Running `docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work
swift:6.0 swift test` now reports 42 / 42 passing (M0a 16 + M0b 18 +
M0c 8).

Updated scarf/docs/IOS_PORT_PLAN.md progress log with the shipped M0c
state and the SQLite3-gating pattern future phases should reuse.

https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
This commit is contained in:
Claude
2026-04-22 22:16:01 +00:00
parent 0fd2ceb9fc
commit 27dc694aeb
7 changed files with 525 additions and 92 deletions
+66 -1
View File
@@ -303,7 +303,72 @@ stderr patterns, and round-trip an actual local file through
iOS's Citadel-based transport lands (M4), it will provide its own env
story — the existing macOS helper stays untouched.
### M0c — pending
### M0c — shipped
**Shipped:**
- 4 portable Services moved to `Packages/ScarfCore/Sources/ScarfCore/Services/`:
- `HermesDataService.swift` (658 lines, SQLite3-backed session/message/activity reader + `SnapshotCoordinator` actor)
- `HermesLogService.swift` (log tailing + parsing, `LogEntry` + `LogLevel`)
- `ModelCatalogService.swift` (models.dev cache reader, `HermesModelInfo` + `HermesProviderInfo`)
- `ProjectDashboardService.swift` (per-project dashboard JSON I/O)
- `HermesFileService.swift`, `HermesEnvService.swift`, `HermesFileWatcher.swift`,
`ACPClient.swift`, and `UpdaterService.swift` stay in the Mac target.
`HermesFileService` holds the big shell-enrichment logic and is the only
non-portable heavyweight — a later phase can port it once iOS has a
clearer story for shell-env-less ACP spawning. `ACPClient` is M1's job
(the `ACPChannel` refactor). `UpdaterService` wraps Sparkle and stays
Mac-only forever.
- The one remaining external consumer that wasn't already importing
ScarfCore (`Features/Settings/Views/Components/ModelPickerSheet.swift`)
now has `import ScarfCore` added.
**Platform guards:**
- **`HermesDataService.swift` is wrapped in `#if canImport(SQLite3)` /
`#endif`** — the whole file. SQLite3 isn't a system module on Linux
swift-corelibs-foundation, and the service is unusable without it.
Apple platforms (the real runtime targets) compile it unchanged. Linux
builds just skip it. Nothing in ScarfCore references
`HermesDataService` from outside that file, so there's no downstream
fallout.
- `ModelCatalogService.swift``import os` / logger definition / logger
call sites all guarded with `#if canImport(os)`. Linux gets silent
logging.
**Test coverage (`M0cServicesTests`):** 8 new tests.
- `HermesLogService.parseLine` exercised via `readLastLines` against a
real local log file with three lines (v0.9.0+ format with session tag,
older format without, and a garbage fallback line). Verifies the
optional session tag handling called out in CLAUDE.md.
- `LogEntry.LogLevel` colour strings pinned (SwiftUI views depend on
them matching colour names).
- `HermesModelInfo.contextDisplay` tested across `1M`, `200K`, `500`,
and `nil` cases; `costDisplay` tested with and without costs.
- `ModelCatalogService` load path exercised end-to-end against a
synthetic `models_dev_cache.json` lookalike — providers sorted
alphabetically, models filtered by provider, `provider(for:)` finds
models both by full scan AND via `provider/model` slash-prefix
fallback.
- Malformed + missing file paths return empty results, no crash.
- `ProjectDashboardService` round-trips a `ProjectRegistry` to disk and
reads back a synthetic `.scarf/dashboard.json`.
**Rules next phases can rely on:**
- The `#if canImport(SQLite3)` gate pattern is established — any future
ScarfCore code that touches SQLite3 directly should use the same
whole-file or whole-block guard rather than trying to abstract SQLite
behind a protocol (overkill; SQLite is reliably available on every
target that can run Hermes client code).
- Services take `ServerContext` in their init and construct their own
transport via `context.makeTransport()`. M0d ViewModels should follow
the same convention when they move to ScarfCore.
- `LocalTransport()` (no-arg init) is the fast path for tests — uses
`ServerContext.local.id`. Test helpers in ScarfCoreTests lean on this
heavily.
### M0d — pending
### M1 — pending
### M2 — pending