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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ All notable changes to PatternSpaceSDK will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows semantic versioning.

## [1.1.0] - 2026-10-09

### Added
- Optional `device.signalSnapshot` read route and `DeviceNamespace.signalSnapshot()` returning schema-1 immutable evidence separately from connection-bound probe authorization. The delegate receives the authenticated `OutputRequestContext`; reads remain available while the JSON source is inactive.
- Optional `pattern.displayProbe` route and `PatternNamespace.displayProbe(_:)` carrying the existing patch shape plus a bounded opaque `expectedContextGuard`. The SDK reuses ordinary patch validation and forwards guard/context to the host for atomic authorization and admission. It never falls back to `displayPatch`.
- Typed source samples, scalar stage observations, separate accepted/published identities, draw/mapping/presentation identities, capture intervals, explicit unknown/omission reasons, and forward-compatible open tokens. Evidence excludes guard and packed frame buffers. Encoded responses are bounded to 256 KiB; each stage retains at most 64 samples with explicit omitted counts. Oversized optional details are marked omitted; oversized identity fails safely.
- Default unsupported delegate hooks and `supportsSignalSnapshot` / `supportsSignalProbe` opt-ins preserve 1.0.0 conformers. Optional `CapabilityFeatures.signalSnapshot` / `signalProbe` have trailing initializer defaults and decode absent from older hosts.

### Changed
- `capabilities.list` filters only unsupported new routes/flags, preserving all existing namespace entries. Unsupported new methods return method-not-found.
- `PatternSpaceProtocolMetadata.sdkVersion` is now `1.1.0`; JSON protocol remains `1.3`. Existing routes, device bit-depth meaning, ACK behavior and global session timeout behavior remain unchanged.

### Fixed
- Both client transports now accept the full 256 KiB SDK response bound, so valid signal snapshots larger than 64 KiB work on native Network.framework endpoints as well as URLSession endpoints. Server incoming request limits remain unchanged.
- Invalid mandatory capture timestamps fail validation rather than being replaced with zero. Unsupported evidence schema versions fail typed validation before evidence or probe authorization is decoded.

### Host integration
- Guards belong to the authenticated connection and stable expected target/configuration/ownership context, independent of snapshot revisions and new patches. Historical responses must have no authorization; stable Refresh must not rotate a current guard. Stale context uses `displayError`, a different connection uses `notAuthorized`, inactive source uses `sourceNotActive`, malformed guard uses `invalidParams`.
- Share only evidence and apply host export redaction to identifiers/optional descriptions. The SDK does not inspect OS profiles, calculate SDI packing, attest physical transport, or supply a per-request timeout.

## [1.0.0] - 2026-10-03

### Added
Expand Down
107 changes: 107 additions & 0 deletions Documentation/SignalTransparency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Signal transparency (SDK 1.1.0)

These optional additions retain PatternSpace JSON protocol 1.3. Evidence has its own schema version, currently 1. No new listener, credential, dependency or physical output behavior is introduced.

## Discovery and compatibility

`CapabilityFeatures.signalSnapshot` and `signalProbe` are optional Boolean fields with nil defaults. Older hosts omit them. Clients enable a feature only when its flag is true and handle method-not-found safely. The dispatcher filters the two unsupported methods from the host-supplied namespace manifest and removes their flags. Every existing namespace entry stays unchanged. `JSONRPCDispatcher.routeManifest` is the complete SDK surface, including optional methods.

Existing server conformers need no additions: `supportsSignalSnapshot` and `supportsSignalProbe` default to false, and the associated methods default to throwing `methodNotFound`. Opt-in hosts implement the flag, method and advertised capability together. No cases are added to existing public enums.

## `device.signalSnapshot`

Accepts absent parameters, `{}` or `[]`. Nonempty or scalar parameters, including null, return `invalidParams`. The authenticated connection identity is supplied by the server, never accepted from JSON. Reading does not require the JSON source to be active and must not admit output or change ownership.

The delegate is:

```swift
func signalSnapshot(context: OutputRequestContext) async throws -> SignalSnapshotResponse
```

The result is a response envelope:

```json
{
"evidence": {
"schemaVersion": 1,
"identity": {
"diagnosticsEpoch": "export-local-epoch",
"revision": 7,
"targetRevision": 2,
"configurationRevision": 4,
"ownershipRevision": 3
},
"capture": { "startedAt": 1791450000, "completedAt": 1791450000.2 },
"freshness": "current",
"source": { "status": "unknown", "fields": [], "samples": [], "totalSampleCount": 0, "omittedSampleCount": 0 },
"appMapping": { "status": "unknown", "fields": [], "samples": [], "totalSampleCount": 0, "omittedSampleCount": 0 },
"sdiFrame": { "status": "notApplicable", "fields": [], "samples": [], "totalSampleCount": 0, "omittedSampleCount": 0 },
"physicalSignal": { "status": "unknown", "fields": [], "samples": [], "totalSampleCount": 0, "omittedSampleCount": 0 },
"omissions": []
}
}
```

Optional `probeAuthorization` is omitted from this evidence-only example. It contains `expectedContextGuard` and `context`, where context declares the diagnostics epoch, optional target lifetime, target revision, configuration revision and ownership revision. A diagnostics revision or new same-owner patch does not change this context. Never export the response envelope or authorization.

## Evidence fields

- `identity` separates latest accepted content from currently published/coalesced content using optional `acceptedContentID` / `acceptedSequence` and `publishedContentID` / `publishedSequence`. It also carries optional `sourceLifetime` and `targetLifetime`. Revision is ordered only within one diagnostics epoch, minted independently of JSON server startup.
- `capture` is an interval in Unix seconds. `freshness` uses `current`, `historical` or `collecting`. Profile/headroom field observations may carry their own intervals; no atomic OS-settings observation is implied.
- Four stages are `source`, `appMapping`, `sdiFrame`, and `physicalSignal`. A stage declares `status`, optional `provenance` and `reason`, typed `fields`, typed `samples`, total sample count and omitted sample count.
- A stage may carry the actual `contentID`, `configurationIdentity`, `mappingIdentity`, `presentationIdentity`, `targetLifetime`, monotonic `drawSequence`, `submittedAt`, `presentedAt`, and separate capture interval. Selected configuration and an older confirmed presentation must remain separately identifiable. Callback time is an app observation, not a measured panel response time.
- `SignalFieldEvidence` uses machine-key `name`, `provenance`, optional `number`, `integer`, `token`, `flag` or `text`, optional `units`, `reason`, and `capture`. Field names, units and tokens are not localized labels. Hosts must declare concrete stage semantics and avoid interpreting one stage's numbers as another stage's physical codes.
- `SignalSampleEvidence` identifies background/rectangle `role` and zero-based `ordinal`. It separates `receivedIntegers` from `receivedValues`, `effectiveRendererInput` and `calculatedCodes`, with optional representation, declared bit depth, geometry, fields and reason. Component structs use `first`, `second`, `third`; containing observations declare RGB versus Y/Cb/Cr and units/layout. Received codes must come from ingress, not a reconstruction of normalized values. Library pattern identifiers/parameters and blank state belong in explicit fields rather than invented RGB samples.

Provenance includes received, calculated, appReported, packed, unknown and notApplicable. Reasons include nonFinite, superseded, readerBusy, timedOut, rectangleLimit and payloadLimit. Physical HDMI link depth, cable codes, dithering and receiver behavior remain unknown without external capture. SDI packed-byte evidence establishes software encoding only. There is no bit-perfect certification.

`SignalToken` is an open Codable raw string. Unknown tokens are preserved on round trip and must be displayed as unknown/unavailable, never mapped to a recognized success. Unknown additive object fields are ignored. `decodeValidated` and client reads reject an unsupported major schema with `SignalSnapshotValidationError.unsupportedSchema(version)` before decoding stage fields or authorization. `validatedData` likewise rejects unsupported schemas. The `isSupportedSchema` convenience remains available for locally constructed evidence; it is not a substitute for validated wire decoding. Existing device-info bit depth keeps its prior meaning and cannot substitute for physical signal evidence.

## Bounds and encoding

Every stage retains at most 64 sample records, in order, with `totalSampleCount` and `omittedSampleCount`. The initializer bounds records; validated decoding rejects oversized or inconsistent wire counts. Missing records are explicitly omitted and must not appear silently absent. No frame buffer field exists.

`SignalSnapshotResponse.validatedData()` verifies schema version, finite numbers and authorization consistency, then encodes sorted-key UTF-8 JSON. It caps the payload at 256 KiB. If optional stage fields/samples exceed the bound, it removes those details, preserves immutable identities/counts, adds top-level `payloadLimit`, and marks affected stages with that omission reason. If the retained identity/envelope still exceeds the limit, encoding throws. The dispatcher also checks the complete JSON-RPC success response bound. `decodeValidated` checks bytes and the schema header before decoding and validates finite fields/counts afterward. Both client transports accept complete incoming SDK messages up to 256 KiB, including the JSON-RPC envelope; server incoming request limits are unchanged. Malformed or oversized host evidence becomes an error, not plausible truncated evidence.

Nonfinite optional scalar/sample inputs become unavailable with `nonFinite`. Mandatory capture intervals retain their original timestamp inputs; NaN/infinity or reversed intervals fail validation as `invalidEvidence` rather than inventing an observation time. Other invalid geometry values fail validated encoding. Consumers should use validated envelope APIs instead of directly decoding untrusted evidence. The SDK does not perform user export redaction: hosts must replace potentially identifying lifetimes/IDs with export-local opaque IDs and remove sensitive names, paths, IPs, serials and sessions from optional descriptions. Share `evidence` only, using a frozen redacted copy.

## `pattern.displayProbe`

Parameters are the flat `PatchParams` shape plus `expectedContextGuard`:

```json
{
"background": { "r": 0, "g": 0, "b": 0 },
"rectangles": [
{ "color": { "r": 1, "g": 0, "b": 0 }, "x": 0, "y": 0, "width": 1, "height": 1 }
],
"bitDepth": 10,
"expectedContextGuard": "opaque-connection-context-token"
}
```

Swift wraps these in `SignalProbeParams(patch:expectedContextGuard:)`. The guard must contain 1–512 bytes of printable non-whitespace ASCII; contents are opaque and have no SDK semantics. Missing, wrong-type, empty, oversized or malformed guards return `invalidParams`. Patch color, geometry, count and bit depth pass the exact shared ordinary patch parser. Unsupported probes return method-not-found even while the JSON source is inactive; supported probes require an active source.

The delegate is:

```swift
func displayProbe(_ params: SignalProbeParams, context: OutputRequestContext) async throws
```

Hosts must compare the authenticated connection, guard and current expected context synchronously on the admission actor immediately before existing output admission, with no suspension between check and commit. The dispatcher cannot validate app-owned context. Stale target/configuration/ownership context throws `displayError`; a guard bound to a different connection throws `notAuthorized`; inactive source throws `sourceNotActive`. Rejected probes must not mutate output, advance ownership or consume render allowance. Never implement unsupported or stale probes using unguarded `displayPatch` fallback.

Issue one stable guard for the eligible authenticated connection and exact context. Refresh, newer diagnostics revisions, same-owner patches, reader timeout and cancelled reads must not rotate it. Real context/eligibility transitions or connection/service replacement retire it. Changes A→B→A must not revive old guards. Capture evidence first, reconcile eligibility/context during response assembly, and omit authorization from historical, superseded or old-connection results. A current partial timeout may retain authorization only when readiness and eligibility are independently established. Validated envelopes reject authorization whose context differs from evidence identity or whose evidence is not current.

The client API sends one explicit sample:

```swift
let response = try await client.device.signalSnapshot()
if response.evidence.isSupportedSchema,
let authorization = response.probeAuthorization {
try await client.pattern.displayProbe(
SignalProbeParams(patch: sample, expectedContextGuard: authorization.expectedContextGuard)
)
}
```

Display success remains ordinary admission, not physical presentation. Submitted SDI work follows the host's existing confirmation contract. SDK methods add no timeout, retry, automatic replay or transport teardown. Cancel cannot recall dispatched/admitted samples. Apps may settle a local UI deadline as outcome unknown while retaining the SDK continuation and serial output queue slot until the real response or disconnect; they must not free that slot, replace the worker or claim the sample was cancelled. Existing output blank/resume and composer ordering stays unchanged.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ let package = Package(
name: "MyTool",
platforms: [.macOS(.v12), .iOS(.v15)],
dependencies: [
.package(url: "https://github.com/caplaz/PatternSpaceSDK.git", from: "1.0.0")
.package(url: "https://github.com/caplaz/PatternSpaceSDK.git", from: "1.1.0")
],
targets: [
.executableTarget(
Expand Down Expand Up @@ -446,6 +446,16 @@ See [Documentation](Documentation/Protocol.md) for the wire-format overview.

The `sourceActive` field is a race-condition guard. In normal PatternSpace operation, changing away from the JSON source stops the server and closes the socket rather than keeping a client connected with `sourceActive: false`.

## Signal transparency (SDK 1.1.0)

Check `capabilities.features.signalSnapshot == true` before reading `client.device.signalSnapshot()`. The response separates immutable `evidence` from optional connection-bound `probeAuthorization`. Share or export evidence only. Schema 1 records source request provenance, app mapping, SDI software encoding and physical-signal unknowns; software observations do not certify a physical cable or panel.

A host opts in with `supportsSignalSnapshot` / `supportsSignalProbe` and implements `signalSnapshot(context:)` / `displayProbe(_:context:)`. Existing delegates compile unchanged: new hooks default to unsupported and new capability fields default to nil. Unsupported hosts omit the new namespace methods and feature flags and return method-not-found. Advertise each flag only for supported functionality; `JSONRPCDispatcher.routeManifest` includes optional routes and dispatch filters them for the host.

When `features.signalProbe == true` and the snapshot provides authorization, an explicit sample uses `client.pattern.displayProbe(SignalProbeParams(patch: patch, expectedContextGuard: authorization.expectedContextGuard))`. The host must validate the authenticated connection and exact expected context atomically with its existing patch admission. The SDK shares ordinary patch validation but never substitutes an unguarded patch write. A success follows the existing admission contract; cancelling a submitted request cannot prevent a sample from appearing. The SDK adds no RPC timeout, automatic retry, or transport changes.

See [Signal transparency](Documentation/SignalTransparency.md) for the schema, bounds, forward compatibility and host obligations. JSON protocol remains 1.3; the evidence schema is independently versioned.

## Development

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@ public final class DeviceNamespace: Sendable {
let data = try JSONEncoder().encode(result)
return try JSONDecoder().decode(DeviceStatus.self, from: data)
}
/// Captures bounded immutable host evidence. Unsupported hosts return methodNotFound.
/// Share `response.evidence` only; the optional authorization is connection-specific.
public func signalSnapshot() async throws -> SignalSnapshotResponse {
struct Params: Encodable {}
let result = try await session.send(method: "device.signalSnapshot", params: Params(), via: transport)
return try SignalSnapshotResponse.decodeValidated(JSONEncoder().encode(result))
}

}
10 changes: 10 additions & 0 deletions Sources/PatternSpaceSDKClient/Namespaces/PatternNamespace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ public final class PatternNamespace: Sendable {
)
}

/// Explicit guarded diagnostic sample; success retains the host's normal admission contract.
/// This method never falls back to displayPatch. Check the signalProbe capability first.
/// Cancellation cannot recall an already submitted sample; no per-request timeout is added.
public func displayProbe(_ params: SignalProbeParams) async throws {
guard SignalProbeParams.isValidGuard(params.expectedContextGuard) else {
throw PSDispatchError(.invalidParams, message: "Malformed expectedContextGuard")
}
_ = try await session.send(method: "pattern.displayProbe", params: params, via: transport)
}

/// Clears the current JSON protocol pattern from the display.
public func clear() async throws {
struct Params: Encodable {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import PatternSpaceSDKCore
import Network

/// WebSocket transport with a serialized delivery fence.
Expand Down Expand Up @@ -154,6 +155,7 @@ final class WebSocketTransport: @unchecked Sendable {
}

let task = URLSession.shared.webSocketTask(with: request)
task.maximumMessageSize = SignalSnapshotResponse.maximumPayloadBytes
socket = .urlSession(task)
task.resume()
receive(from: task, epoch: epoch)
Expand All @@ -162,7 +164,7 @@ final class WebSocketTransport: @unchecked Sendable {
private func openNativeWebSocket(to endpoint: NWEndpoint, token: String?, epoch: UInt64) {
let webSocketOptions = NWProtocolWebSocket.Options()
webSocketOptions.autoReplyPing = true
webSocketOptions.maximumMessageSize = 65_536
webSocketOptions.maximumMessageSize = SignalSnapshotResponse.maximumPayloadBytes
if let token {
webSocketOptions.setAdditionalHeaders([("Authorization", "Bearer \(token)")])
}
Expand Down
Loading
Loading