README.md (27532B)
1 # Tera iOS App 2 3 Tera is a public iOS 18 app for discovering and publishing local farm 4 updates, asks, in-person events, and food listings over Nostr. The product has 5 exactly two bottom tabs: Today for discovery and Add for authored operations. 6 7 The current public release is `0.1.0-alpha`. 8 9 Tera owns the application's Rust validation, runtime transitions, durable 10 authored operations and application FFI as well as the native iOS host. Shared 11 domain types, signing protocols, transport and storage mechanics remain in the 12 public foundation packages. The target behavior keeps cached Today and local 13 Add usable independently of network readiness; the native host presents the Rust contracts and supplies 14 Apple platform capabilities. 15 16 Application Rust packages belong under `core/crates` in the single root Cargo 17 workspace. Installed native artifacts use the owned `tera_ffi` and matching 18 `tera_bindgen` packages, with shared packages at their exact foundation pins. 19 The five creation families, Today/Add tabs, installed identity and persisted 20 operation formats remain compatible with the original app. 21 22 Today refresh starts from the newest relay window without an age cutoff or a 23 latest-event watermark. Each search requests the same seven supported kinds 24 with at most eight pages of 500 observations. Saved-feed pagination reads local 25 posts; the separate **Search older posts** action explicitly continues remote 26 discovery while saved posts remain usable. Continuations belong to the active 27 network, account and store generation. Incomplete relay responses remain visible 28 through the search, including when a later page completes. A saturated timestamp 29 can leave gaps among same-time posts, so advancing to older posts does not prove 30 complete relay history. Refresh starts a new search from the newest window. 31 32 Publication keeps the original captured form, signed bytes when available, and 33 frozen relay evidence after the delivery window or shared attempt cap is 34 exhausted. Authentication, quota and malformed-event refusals require attention 35 instead of an automatic retry. Explicit continuation respects shared claims and 36 retry-after limits plus bounded exponential backoff from one to sixty seconds, 37 with restart-stable jitter. Rust rechecks the time window before signing and 38 delivery; the native host presents its decision and offers status refresh. 39 Existing late-fact reconciliation remains local and never also sends a retry. 40 No deadline extension, new signature, destination amendment or retry worker is 41 implied by recovering a saved submission. 42 43 `TeraFFI/producer.toml` separately governs the owned Tera FFI producer. 44 After staging its source inputs, `make ffi-source-write ffi-source-check` 45 captures and checks the exact source tree, foundation lock, target, features and 46 toolchains under extbuild output. Select a supported target with `FFI_TARGET`. 47 This is local source evidence. `make ffi-bootstrap` installs the matching 48 native artifacts and records their exact source tree, foundation and hashes. 49 50 `make ffi-candidate-build ffi-candidate-check` builds the owned device, 51 simulator and host libraries, generates matching Swift and API outputs, and 52 verifies the staged XCFramework and provenance. Candidates remain under 53 extbuild output; this command does not install them into the native app. 54 55 ## Requirements 56 57 - macOS with Xcode and an iOS 18-or-newer simulator 58 - XcodeGen 59 - Rust `1.97.1-aarch64-apple-darwin` with `llvm-tools` and the iOS device and simulator targets 60 - SwiftFormat and the locked Python verifier installed by bootstrap 61 - JDK 21 on Apple silicon for the generated Kotlin binding smoke 62 - `cargo-extbuild` configured for the checkout 63 64 Physical-device development additionally requires one exact paired, connected, 65 unlocked iPhone with Developer Mode enabled, an Apple development team, and a 66 generated xcconfig below the extbuild-owned DerivedData root. The governed 67 parent workspace supplies those machine inputs. The standalone script refuses 68 name-only destinations, unsigned builds, non-Debug physical builds, and 69 xcconfig files outside the managed output root: 70 71 ```sh 72 TERA_IOS_PHYSICAL_AUTOMATION=1 \ 73 TERA_IOS_DEVELOPMENT_TEAM=ABCDEFGHIJ \ 74 cargo extbuild run -- scripts/xcode.sh physical-app-build \ 75 id=00000000-0000000000000000 \ 76 "$XCODE_DERIVED_DATA/radroots-ios-device/config/device.xcconfig" 77 ``` 78 79 The values above are placeholders. Device identities, teams, endpoints, and 80 certificate material are never checked into this public repository. 81 82 ## Bootstrap and verify 83 84 The first bootstrap requires network access. It resolves exact foundation 85 dependencies, builds the owned UniFFI XCFramework and Swift 86 bindings, bootstraps the pinned Kotlin smoke dependencies, resolves exact Swift 87 package revisions, and generates the Xcode project: 88 89 ```sh 90 cargo extbuild doctor 91 make bootstrap 92 ``` 93 94 After bootstrap, the complete package and Xcode build/test lane uses the 95 resolved revisions without automatic dependency updates: 96 97 ```sh 98 make verify 99 ``` 100 101 The complete lane includes the repository-owned Swift formatting and lint 102 policy and the pinned Linux x86_64 shared-Rust runner. Run those focused checks 103 independently with `make swift-quality` and `make linux-shared-rust`; both keep 104 their build output under extbuild. 105 106 `make kotlin-smoke` generates Kotlin from the exact installed producer's native 107 host library and executes its scope, error, cancellation, subscription, shutdown, 108 and media-ownership tests through JNA. The pinned Gradle wrapper, dependency locks 109 and checksum verification are owned by this repository; generated Kotlin, native 110 test results and Gradle output remain under extbuild. Verification uses offline 111 dependency resolution after `make kotlin-smoke-bootstrap` or `make bootstrap`. 112 This test-only JVM harness is part of `make verify` and does not qualify Android 113 UI or a published release. 114 115 `make swift-quality` also applies the exact checked SwiftLint complexity 116 baseline and the repository-owned Swift/Python maintainability ratchet. New 117 Swift files are capped at 600 physical lines, new Python files at 800, and new 118 Python functions at complexity 10. Existing larger files and functions are a 119 closed, non-growing inventory; the newly separated user-message classifier 120 and package verification modules must remain below the new-file limits. Run 121 `make maintainability-check` for the narrow size and Python-complexity gate. 122 123 Use `SIMULATOR_NAME="Device Name" make verify` when the default simulator is 124 not installed. `make clean` removes the external native candidate cache while 125 preserving Cargo incremental output, the current installation, source and locks. 126 127 The focused local-social scenario starts bounded loopback Nostr and Blossom 128 fixtures, then exercises the real app stores and installed Rust FFI on one 129 exact simulator. The test saves an unverified photo draft, relaunches, retries 130 from the durable draft against the enabled fixture, publishes all five product 131 types, and proves Today and the outbox survive another relaunch: 132 133 ```sh 134 TERA_IOS_UI_TEST_RUN_ID=local-social-example \ 135 cargo extbuild run -- scripts/xcode.sh local-social-ui-test \ 136 'platform=iOS Simulator,id=SIMULATOR-UDID' \ 137 local-social-example 138 ``` 139 140 This harness is Debug-only, binds only explicit loopback ports, records bounded 141 protocol evidence under the extbuild results root, and cannot become a 142 production endpoint fallback. Its typed isolated-loopback mode is the only 143 mode permitted to use automated user presence or test secret policy. Public 144 and physical qualification retain normal Apple user presence, remain optional, 145 and are not claimed by the deterministic simulator lane. 146 147 The app and XCUITest runner admit the simulator endpoints through closed typed 148 policies. The fixture creates both servers through one observable loopback 149 connection factory, rejects and counts every non-loopback peer, and records 150 accepted and rejected socket counters in its bounded evidence snapshot. 151 152 The loopback Blossom fixture admits uploads only with the exact signed BUD-11 153 HTTP authorization produced by the installed Rust runtime: kind `24242`, 154 bounded non-empty human content, one upload action, one exact SHA-256, one 155 lowercase domain-only server scope, and one canonical expiration whose 156 lifetime is at most 300 seconds. Event ID and signature verification precede 157 admission, and the relay explicitly rejects authorization events. The shared 158 mutation corpus contains only field-mutation instructions; it persists no 159 private material or signed authorization event. 160 161 Passing `accessibility` as the final launcher argument runs Apple's 162 accessibility audit over every progressively disclosed Add composition at the 163 largest accessibility text size with Reduce Motion enabled. The corresponding 164 fixture verifier requires zero publication and upload effects: 165 166 ```sh 167 TERA_IOS_UI_TEST_RUN_ID=local-social-accessibility \ 168 cargo extbuild run -- scripts/xcode.sh local-social-ui-test \ 169 'platform=iOS Simulator,id=SIMULATOR-UDID' \ 170 local-social-accessibility \ 171 accessibility 172 ``` 173 174 Element-bound clipping findings remain fatal. The harness tolerates only 175 Xcode's elementless clipping diagnostics and narrowly identified contrast 176 false positives for disabled controls, system-chrome overlap, and the 177 black-on-white Submit button. 178 179 Passing `persona` runs the strict five-persona, 15-attempt deterministic 180 local-social matrix serially. Each persona receives a fresh run-scoped native 181 identity and isolated durable store while one bounded loopback Nostr relay and 182 Blossom service record exact event, media, retry, and subscription evidence: 183 184 ```sh 185 TERA_IOS_UI_TEST_RUN_ID=local-social-persona-run-001 \ 186 cargo extbuild run -- scripts/xcode.sh local-social-ui-test \ 187 'platform=iOS Simulator,id=SIMULATOR-UDID' \ 188 local-social-persona-run-001 \ 189 persona 190 ``` 191 192 The persona fixture and historical v1 result remain strict, deny-unknown 193 contracts. Each completed attempt now also emits one bounded canonical 194 `persona-attempt-evidence.v1` xcresult attachment bound to the exact XCUITest 195 target and identifier, test action and configuration, source commit and tree, 196 app-build digest, simulator, run, persona, attempt, endpoint policy, and visible 197 UI outcome. The attachment never retains a raw secret, signed authorization 198 event, or raw event content. Validation, retry, relaunch, retention, Today, 199 connection, subscription, event, upload, and retrieval fields come from the 200 executed UI path and monotonic fixture-snapshot deltas. The v2 result is 201 reconstructed only from the exact 15 measured attachments and is cross-checked 202 against the final fixture totals; a non-loopback attempt fails the run. 203 204 The standalone fixture and result verifier runs under the exact Python 3.14.7 205 and `jsonschema` 4.26.0 environment locked in 206 `scripts/persona-verifier/uv.lock`. `make bootstrap` installs that exact lock 207 through extbuild once; qualification and package checks then use only 208 `--offline --frozen` resolution. Every JSON input is read with a 209 maximum-plus-one bound before decoding, schema files pass Draft 2020-12 210 meta-validation, semantic fixtures must agree with their schemas, exported 211 attachment inventory is exact, and result-bundle hashing uses a 212 domain-separated length-framed preimage with bounded paths, entries, files, 213 and aggregate bytes. 214 215 The test uses ordinary visible controls, native-generated signing identities, 216 real app stores, and generated Rust FFI. This is deterministic non-human 217 conformance evidence. It does not claim human usability, demographic or 218 population validity, observed VoiceOver-user experience, release readiness, 219 or production qualification. 220 221 ## Package surface 222 223 `Package.swift` publishes the `TeraApp` library used by the generated Xcode 224 application wrapper. It pins AppleKit by exact HTTPS Git revision and consumes 225 the locally bootstrapped `TeraFFI.xcframework`. Ordinary Xcode compilation 226 never writes repository source: a read-only preflight rejects missing or stale 227 FFI artifacts and directs the developer to run `make bootstrap`. 228 229 The Rust source lock, generated bindings, XCFramework hashes, provenance, Swift 230 package locks, privacy manifests, and public API snapshots are checked as part 231 of the release lane. 232 233 `make package-contract-check` evaluates the Swift package manifest and parses 234 the TOML, plist, JSON, xcconfig, project-package, and lock inputs as structured, 235 bounded data. It also runs the locked fixture and verifier unit suites. 236 237 For focused UI iteration, `make ui-test UI_TEST_SELECTOR=TeraAccessibilityUITests/testFormAskAtLargestTextWithReduceMotionAndLocalSave` 238 selects an existing owned class and method through the same artifact, package, 239 project and output-routing checks. An omitted selector runs the full UI target. 240 Unknown classes, methods, malformed selectors and extra arguments fail before 241 Xcode starts. Focused runs do not replace complete required qualification. 242 243 The package check evaluates Cargo's workspace graph and rejects members or local 244 dependencies outside this standalone repository, including implicit sibling 245 checkouts. The default Rust lane selects `tera_core`, `tera_ffi`, and 246 `tera_bindgen`; `tera_wasm` remains non-default. The resolved graph must use 247 the exact shared foundation lock, activate the FFI mobile-social profile, and 248 contain no retired source-lock shim or old mobile producer. Human specifications 249 and execution evidence remain outside the capsule and are never required by 250 these checks. 251 Comments, examples, unreachable source, and arbitrary matching text cannot 252 satisfy a behavior-bearing package assertion; application behavior is proven 253 by the compiled Swift and simulator test lanes. 254 Legacy identifiers are governed by `test-fixtures/legacy-identifiers.v1.json`. 255 Each exact name/path/count refers to an owner, reason, reader, and removal 256 condition. Unknown names, expanded or stale exceptions, old app declarations, 257 and changed display names fail the package check. Shared Apple/foundation APIs, 258 installed identities, persisted namespaces, and existing compatibility readers 259 retain their names. Generated FFI names are checked through owned generation 260 and API freshness; dependency caches and generated binaries are not source 261 exceptions. The policy is part of the native producer's declared input tree. 262 263 The contract also binds the exact Ruff development tool, both maintainability 264 baselines, and their executable verifier, so local lint behavior cannot drift 265 with an ambient Python installation. 266 267 The unsigned release-evidence lane also regenerates a deterministic CycloneDX 268 SBOM from the locked Rust and Swift dependency graphs and binds it to the 269 checked-in locks, API snapshots, privacy inputs, Xcode project, XCFramework 270 provenance, and exact public repository identity: 271 272 ```sh 273 make release-evidence-write 274 make release-preflight 275 ``` 276 277 Run the write target only when an owned release input changes. The preflight 278 is read-only and rejects stale generated evidence. Signing, tagging, 279 publication, and deployment remain separate operations. 280 281 Revision preparation retains a request identity and immutable replacement form 282 before signing or delivery. Retrying an uncertain preparation returns the same 283 replacement and retraction graph; changed input requires a new intentional 284 request. Regular posts retain the original source event and a separate ordered 285 retraction child. Current local card overlays also carry the exact source draft 286 key, so revision editing can recover the retained form from either a legacy 287 draft or a scoped submission. Missing forms and mismatched signed sources fail 288 closed; a displayed operation ID is never treated as proof of source ownership. 289 290 Regular revision delivery retains the replacement's exact relay policy on its 291 linked retraction child. Each deletion destination requires replacement evidence 292 from that same destination; another relay's success or aggregate completion does 293 not grant permission. Retries reuse the original signed child and full destination 294 list, withholding ineligible and already-satisfied destinations. Cancelling a 295 partial revision stops both pending branches and retains observed effects across 296 reopen. Historical revision children without an exact parent link remain held; 297 their signed bytes and operation identities are never rewritten or rebound to a 298 guessed replacement. This restriction also holds historical independent deletions 299 using the reserved revision reason. Other independent retractions remain separate. 300 301 Saved revisions expose their original event, replacement and linked retraction 302 in Drafts & outbox. Selected details reconstruct per-relay observations and local 303 stop state from durable records, including uncertain acknowledgments and partial 304 effects after restart. Resume and stop actions are checked against current Rust 305 policy; inspecting or acting on a revision does not replace composer editing. 306 Meeting an Any relay policy does not imply completion at every saved destination, 307 and even a fully accepted retraction remains a request, not proof of erasure. 308 309 Food Availability remains a public announcement. Shared constructors validate 310 exact decimal strings, matching quantity/price units, currency shape and 311 active/sold status. A three-letter currency is not a verified financial asset; 312 bags and bunches carry no inferred conversion. The native form preserves partial 313 draft text. A new Submit or revision capture translates complete locale decimal entries into exact 314 canonical strings before capturing the saved revision, without floating-point 315 conversion. Canonical dots remain valid after a locale change; grouping, mixed 316 separators, signs, exponents and incomplete entries are not guessed. Translation 317 does not alter an existing submission or retry. A new intentional announcement 318 retains its own identifier and operation even when its visible details match. 319 320 Retraction requires the exact signed original in the current account's store. 321 Rust verifies its signature, author, event kind, address and product card identity 322 before capture and again before signing, local admission and delivery. A native 323 card or an older saved signature alone does not authorize those effects. Missing 324 source evidence holds publication while retained receipts and cancellation remain 325 available. Already retracted originals remain eligible evidence for repeated 326 requests; their visibility does not grant or remove authorship. 327 328 Addressable publication keeps its full kind, author and identifier coordinate. 329 Local requests acquire durable ownership before signing or publication, and a 330 superseded request cannot regain that ownership by retrying after restart. 331 If the known current revision changes, publication is held while the captured 332 form, signed bytes and relay evidence remain available. A new request must be 333 reviewed against the current revision. Addressable replacement does not create 334 a broad deletion request. 335 336 Equal-second replacements use the shared Nostr event-ID tie-break rule. Tera 337 preserves the captured event timestamp and never advances it artificially to 338 win a replacement. A timestamp ahead of the current clock, or one that loses 339 to the known revision, holds publication for review. A forward clock jump 340 does not extend the saved delivery window. Review the device clock and current 341 revision before choosing another intentional request. Civil calendar dates 342 remain unchanged. This local policy cannot establish globally accurate time 343 or prevent an unseen remote replacement. 344 345 Publication stage labels report saved local, signing, media and relay facts. Completion means the operation met its saved relay policy; it does not mean every relay accepted it or will retain it permanently. Mixed legacy settlement counts remain visible alongside the current stage, including pending, unknown, exhausted, failed and stopped work. Stopping local work preserves remote evidence, and retraction is a deletion request rather than proof of erasure. Submit and continuation controls follow the saved typed retry decision; status checks remain available without authorizing new effects. 346 347 Storage startup retains the existing identity-scoped database names. An 348 incomplete pair requires recovery and is never replaced with an empty member. 349 The shared SQLite owner checks both existing schemas and the expected source 350 generation before writable setup; unsupported versions or incompatible state 351 remain typed failures with original evidence retained. Protected-data absence 352 also fails before opening storage. Startup does not rename directories, import 353 arbitrary paths, repair permissions or silently replace identity custody. 354 355 Interrupted calendar rebuilds reconcile through the existing storage owner's 356 durable records, retaining legacy bytes and publication identities. Lazy 357 prepared-photo recovery validates an existing destination before accepting it; 358 conflicts require recovery without overwriting either copy. A verified legacy 359 copy is installed only after confirming destination absence, and the old copy 360 remains available across repeated restarts. Unpublished partial staging is not 361 treated as a completed photo or discarded by migration. 362 363 Local application backup uses the exact shared database owner's capture, 364 verification and finalization capability. An explicit backup-enabled startup 365 prepares the account backup root through the native file owner; ordinary startup 366 does not create it. The caller closes its previous session before selecting this 367 startup mode. Backup admission requires an idle runtime, waits for earlier owner 368 writes to settle (including canceled commands), and excludes mutations until the 369 attempt finishes. It binds the account, source generation, request ID, 370 format version, database digests and sizes, and all required authored-media 371 digests and byte counts. Native media leases are immutable and survive removal 372 of the original prepared file. They live in the protected account backup tree; 373 directory custody metadata is applied before immutable file creation. Missing or 374 conflicting media refuses completion. 375 376 The application manifest is bounded to 16 MiB, each retained media member to 377 64 MiB, and the reference inventory to the existing 65,536-reference traversal 378 budget. The caller supplies a positive aggregate byte limit. These are processing 379 bounds, not a disk-space reservation. Candidate and complete manifests use 380 create-only durable installation. Retrying an exact persisted candidate verifies 381 its original snapshot and leases, even if live state has since changed. An 382 interrupted attempt retains incomplete owner staging and media leases; absence 383 of a complete manifest is never success. Backups stay local, excluded from OS 384 backup, with host file protection; this adapter neither exports Keychain secrets 385 nor restores or republishes historical operations. Existing conservative media 386 cleanup retains the backup tree. Physical power-loss behavior is not inferred 387 from simulator interruption tests. 388 389 Cold restore is a separate explicit host operation against a completed, 390 account-bound local backup. It requires exclusive native file maintenance, no 391 live application process users, available protected data and a known-empty OS 392 transfer inventory. The canonical database owner verifies and installs both 393 members, closes and reopens them; the native file owner restores only verified 394 immutable media. There is no arbitrary-path restore or secret export. 395 396 A bounded external guard is installed before database replacement. Ordinary 397 startup refuses that guard; guarded startup requires a matching durable restore 398 barrier. Interrupted or ambiguous installation retains all evidence and requires 399 explicit repair with supported software. Never delete the guard, replace a 400 missing member with an empty database, or use an older unsupported application 401 to bypass recovery. Existing binaries do not retroactively acquire this guard. 402 Guarded native startup loads saved transport preferences without adopting 403 bootstrap overrides or stopping original operations. Review waits for canceled 404 owner writes to finish before it reads the complete inventory. 405 406 Restored signing, upload authorization and delivery remain held until the user 407 checks original destinations, reviews the complete current operation inventory, 408 and separately confirms resume in Drafts & outbox. Each destination check is 409 bounded to two pages of 64 events and verifies actual event identity. Offline or 410 partial results cannot authorize resume; a completed query that did not observe 411 an event is not proof that it was never accepted. The inventory admits at most 412 4,096 records and 4,096 pending destinations within a 64 MiB payload budget. 413 Changes invalidate the prior review. Resume preserves original operation IDs, 414 signed bytes and outcomes; it does not itself sign, upload or deliver anything. 415 416 Storage pressure is reported separately from native transfer receipt capacity. 417 A failed save can have committed before its acknowledgement was lost; reconcile 418 the original composer or operation rather than creating a replacement. Settings 419 accept cache budgets from 16 MiB through 2 GiB and 1 through 10,000 artifacts. 420 Inbound cache admission also enforces the same upper bounds, including decoded 421 policies; smaller internal budgets remain valid for constrained callers. 422 423 Settings offers explicit cached-photo cleanup for the selected context. Each 424 action invalidates at most 64 least-recently-used cache entries and reports 425 remaining entries and retained file candidates, without claiming bytes freed. 426 Physical deletion requires complete bounded cross-context ownership proof under 427 the existing file/projection fence. Unknown owners or unsettled writes retain 428 files. Draft staging, pending operations, transfer receipts and backup media are 429 not cleanup targets. Invalidation must persist first: if the store is completely 430 full, free device space before trying again. Cache cleanup cannot resolve the 431 independent bounded transfer-receipt envelope limit. 432 433 434 Public posts and profile updates are linked to the public Nostr key and may be 435 retained by relays and other people. Only deliberately included location text 436 (such as a public venue or address) is posted; camera/library images have 437 sensitive metadata removed before staging. The app does not automatically 438 publish device location and has no tracking or automatic telemetry endpoint. 439 Its privacy manifest covers public names, identifiers, deliberately supplied 440 addresses, photos and other posted content for application functionality. 441 442 File metadata is accessed only for owned storage. The generic file owner 443 checks available capacity locally before a write and reports the existing 444 insufficient-space outcome. This check is conservative and does not reserve 445 space or replace actual write/quota error handling. Capacity values stay local. 446 447 Diagnostics exports contain bounded status codes and counts; prepare and review 448 one before explicitly sharing it. [Radroots Support](mailto:support@radroots.org) 449 is the canonical contact for support, privacy and removal inquiries. Settings 450 opens the system email handler only after an explicit choice; no report or attachment 451 is sent automatically. Provisioning/monitoring and operational reporting drills 452 remain distribution prerequisites, not engineering-test outcomes. 453 454 Removing a local signing key does not erase public copies. Review and stage any 455 desired signed deletion requests before removing the key. Requests and relay 456 acknowledgements cannot prove erasure by every recipient. The implemented local 457 removal flow preserves its existing source and user-presence checks; it does not 458 claim a decentralized-account policy exemption or App Store approval.