Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-14, macos-15]
os: [macos-15]
# macos-14 dropped: ships Swift 5.10 by default; Package.swift requires
# swift-tools-version 6.0+ (the DD-338 W5 SPM dep on stallari-mcp-helpers-swift
# introduces a transitively-required 6.0 toolchain). Re-enable with
# maxim-lobanov/setup-xcode@v1 + xcode-version: '16.0' if Swift 5.10
# backport is ever in scope.

steps:
- name: Checkout
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2026-05-24

### Changed

- **DD-338 Phase C Wave 5:** consolidate `_meta:` envelope emission onto the
canonical `stallari-mcp-helpers-swift` SPM dep (`MCPHelpers` module v0.1.0).
Local `Sources/AppleNotesBlade/MetaEnvelope.swift` (160 LOC) deleted; functionally
equivalent but wire-shape now matches the cross-language canonical:
- `redactions` and `next_cursor` are ALWAYS emitted (defaults to `[]` and JSON
`null` respectively). Hand-rolled v1 omitted these when empty/nil.
- `filtered_by` is alphabetically sorted by the formatter (caller pre-sort is
no longer load-bearing; existing pre-sorts remain harmless).
- `formatMetaLine` now throws (encoding-failure surface).
- Catalog declarations flipped `audit_surface: minimal → structured` for the 3
tools emitting `_meta` envelopes (`apple_notes_list_folders`,
`apple_notes_list_notes`, `apple_notes_search_notes`). Reconciles drift
between code shipped 2026-05-23 PR #2 and catalog declaration.
- Platform bump `.macOS(.v13) → .macOS(.v14)` to satisfy the
`stallari-mcp-helpers-swift` minimum platform.
- Retained `metaQueryDigest(_:)` and `Duration.toMilliseconds()` as blade-local
helpers in a new `Sources/AppleNotesBlade/MetaHelpers.swift` — these are NOT
part of the canonical surface.

### Removed

- `Sources/AppleNotesBlade/MetaEnvelope.swift` (replaced by the canonical
`MCPHelpers` SPM dep).

## [0.2.0] - 2026-05-23

### Added

- First Swift-blade `_meta:` envelope emission across 3 tools
(`apple_notes_list_folders`, `apple_notes_list_notes`,
`apple_notes_search_notes`). DD-338 Phase C Wave 5 audit_surface promotion.
7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import PackageDescription
let package = Package(
name: "apple-notes-blade-mcp",
platforms: [
.macOS(.v13)
.macOS(.v14)
],
products: [
.library(
Expand All @@ -33,13 +33,18 @@ let package = Package(
// purely for resolution compatibility. The runtime SQLite client
// exposed by the package is API-compatible across traits.
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.3", traits: ["SQLCipher"]),
// Canonical _meta: envelope helpers (DD-338 Phase E.swift). Replaces
// the previously hand-rolled Sources/AppleNotesBlade/MetaEnvelope.swift
// (deleted in DD-338 Phase C Wave 5).
.package(url: "https://github.com/Groupthink-dev/stallari-mcp-helpers-swift", from: "0.1.0"),
],
targets: [
.target(
name: "AppleNotesBlade",
dependencies: [
.product(name: "MCP", package: "swift-sdk"),
.product(name: "SQLite", package: "SQLite.swift"),
.product(name: "MCPHelpers", package: "stallari-mcp-helpers-swift"),
]
),
.testTarget(
Expand Down
160 changes: 0 additions & 160 deletions Sources/AppleNotesBlade/MetaEnvelope.swift

This file was deleted.

56 changes: 56 additions & 0 deletions Sources/AppleNotesBlade/MetaHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// MetaHelpers.swift
//
// DD-338 Phase C Wave 5 — blade-local helpers that complement the canonical
// `MCPHelpers` module (SPM dep at
// https://github.com/Groupthink-dev/stallari-mcp-helpers-swift). The previous
// Sources/AppleNotesBlade/MetaEnvelope.swift hand-rolled the entire envelope
// surface; it was deleted when the canonical lib shipped (v0.1.0) and the
// blade migrated onto the SPM dep. This file retains only the two helpers
// that are NOT part of the canonical surface:
//
// - `Duration.toMilliseconds()` — convert `ContinuousClock.Duration` to Int
// milliseconds for `MetaEnvelope.latencyMs` (canonical lib leaves clock
// marshalling to the caller).
// - `metaQueryDigest(_:)` — SHA-256 short-digest of a verbatim query string
// for `filtered_by` `query=` audit entries. Blade-domain helper (search
// tools use it); not generic enough for the canonical lib.
//
// All envelope construction, formatting, and appending now flows through
// `MCPHelpers.MetaEnvelope`, `MCPHelpers.formatMetaLine`, and
// `MCPHelpers.appendMeta`.

import CryptoKit
import Foundation

// MARK: - Latency timing

extension Duration {
/// Convert a `ContinuousClock.Duration` (or any `Duration`) to integer
/// milliseconds. Truncates sub-millisecond fractions. Used to populate
/// `MetaEnvelope.latencyMs`.
public func toMilliseconds() -> Int {
let comps = self.components
// attoseconds = 1e-18 s; 1 ms = 1e-3 s = 1e15 attoseconds.
let msFromSeconds = comps.seconds * 1000
let msFromAttoseconds = comps.attoseconds / 1_000_000_000_000_000
return Int(msFromSeconds + msFromAttoseconds)
}
}

// MARK: - Query digest

/// Compute a SHA-256-based 12-character digest of a query string, suitable for
/// `filtered_by` `query=` audit values. Privacy-safe + token-bound +
/// collision-resistant within session window. Mirrors W3 OQ-6 ratification for
/// `cf_d1_query`. The digest is non-reversible — the assembler audit trail
/// records that a search was performed without recording the verbatim query
/// bytes.
public func metaQueryDigest(_ query: String) -> String {
let data = Data(query.utf8)
let digest = SHA256.hash(data: data)
let base64 = Data(digest).base64EncodedString()
// Strip non-alphanumeric chars (`+`, `/`, `=`) to keep the audit value
// path-safe and grep-friendly, then take the first 12 characters.
let stripped = base64.filter { $0.isLetter || $0.isNumber }
return String(stripped.prefix(12))
}
5 changes: 3 additions & 2 deletions Sources/AppleNotesBlade/Tools/ListFolders.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import MCP
import MCPHelpers

/// Handler for `apple_notes_list_folders`. Optional `account_id` filter.
///
Expand Down Expand Up @@ -29,8 +30,8 @@ public struct ListFoldersHandler: Sendable {
let meta = MetaEnvelope(
matchedTotal: folders.count,
returned: folders.count,
filteredBy: filteredBy,
latencyMs: elapsed.toMilliseconds()
latencyMs: elapsed.toMilliseconds(),
filteredBy: filteredBy
)
return makeResultWithMeta(
payload: ListFoldersResponse(folders: folders),
Expand Down
5 changes: 3 additions & 2 deletions Sources/AppleNotesBlade/Tools/ListNotes.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import MCP
import MCPHelpers

/// Handler for `apple_notes_list_notes`. Index-only — never decodes bodies.
///
Expand Down Expand Up @@ -55,8 +56,8 @@ public struct ListNotesHandler: Sendable {
let meta = MetaEnvelope(
matchedTotal: notes.count,
returned: notes.count,
filteredBy: filteredBy,
latencyMs: elapsed.toMilliseconds()
latencyMs: elapsed.toMilliseconds(),
filteredBy: filteredBy
)
return makeResultWithMeta(
payload: ListNotesResponse(notes: notes),
Expand Down
8 changes: 6 additions & 2 deletions Sources/AppleNotesBlade/Tools/ResultBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import MCP
import MCPHelpers

/// JSON encoder used by all tool handlers. ISO-8601 dates with fractional
/// seconds; pretty-print disabled (consumer-facing JSON, not human-edit).
Expand Down Expand Up @@ -34,12 +35,15 @@ func makeResult<T: Codable>(payload: T) -> CallTool.Result {
/// (no envelope on errors per Wave 3 OQ-7 ratification).
///
/// Use at every Track-promoted handler site to avoid per-handler boilerplate around
/// encode + envelope-append. See `MetaEnvelope.swift` for the helper module.
/// encode + envelope-append. Envelope construction + formatting uses the canonical
/// `MCPHelpers` SPM dep (DD-338 Phase C Wave 5; replaces former hand-rolled
/// Sources/AppleNotesBlade/MetaEnvelope.swift).
func makeResultWithMeta<T: Codable>(payload: T, meta: MetaEnvelope) -> CallTool.Result {
do {
let data = try makeToolEncoder().encode(payload)
let json = String(data: data, encoding: .utf8) ?? "{}"
let text = appendMeta(json, formatMetaLine(meta))
let line = try formatMetaLine(meta)
let text = appendMeta(json, line)
return CallTool.Result(content: [.text(text: text, annotations: nil, _meta: nil)])
} catch {
return errorResult(.internalError("encode_failure"))
Expand Down
5 changes: 3 additions & 2 deletions Sources/AppleNotesBlade/Tools/SearchNotes.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import MCP
import MCPHelpers

/// Handler for `apple_notes_search_notes`. v0.1.0 implementation: SQL `LIKE`
/// against `ZTITLE1` and `ZSNIPPET` — never opens body bytes. Real FTS via
Expand Down Expand Up @@ -65,8 +66,8 @@ public struct SearchNotesHandler: Sendable {
let meta = MetaEnvelope(
matchedTotal: results.count,
returned: results.count,
filteredBy: filteredBy,
latencyMs: elapsed.toMilliseconds()
latencyMs: elapsed.toMilliseconds(),
filteredBy: filteredBy
)
return makeResultWithMeta(
payload: SearchNotesResponse(query: query, results: results),
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppleNotesBlade/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ extension AppleNotesBlade {
/// SemVer string for the library. Bump together with the git tag.
/// Pre-release suffixes (`-rc1`) are allowed; consumers should pin by SHA
/// not by version while the suffix is non-empty.
public static let semver = "0.2.0"
public static let semver = "0.3.0"
}
Loading
Loading