| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
See what changed.
Diffuse answers one question: what changed on this device between these two points in time?
It is a local-first, five-platform native product: macOS, iOS, iPadOS, watchOS, and Android. The four Apple apps share a capability-driven Swift engine. Android is a fully native Kotlin/Jetpack Compose implementation with its own domain engine. The two families interoperate through schema-v1 JSON snapshots and golden fixtures, not a cross-platform UI toolkit or runtime bridge.
Snapshots never leave the device unless you explicitly export one. There is no account, no cloud, no sync protocol, and no telemetry. GitHub is the only backend this repository talks to—and only for source, CI, and unsigned artifacts.
Site: hoangsonww.github.io/Diffuse-Native-Apps
Take a snapshot. Use your machine. Take another. Diffuse diffs the two:
Every capability carries its own schema. The diff engine, the store, search, export, and the UI are generic: adding something new to observe is a typed model, a collector, a registry line, and a test. No new screens.
flowchart LR
One[Take snapshot A] --> Use[Use the device]
Use --> Two[Take snapshot B]
Two --> Diff[Schema-driven diff]
Diff --> Review[Review severity, values, and timeline]
Review --> Export[Optionally share a redacted report]
A snapshot is not an opaque dump. Each section includes the schema required to display and compare it: stable identity, property labels, units, comparison rules, severity, ordering, and privacy classification. An app can therefore open an older snapshot—or a capability it no longer collects—and still render it honestly.
flowchart TD
Snapshot --> Section[Capability section]
Section --> Schema[Travelling schema]
Section --> Entities[Observed entities]
Schema --> Rules[Comparison + severity rules]
Schema --> Privacy[Privacy classifications]
Entities --> Values[Typed property values]
A snapshot is one pretty-printed JSON file. Every section carries the schema needed to render and compare it, so nothing depends on the app version that happens to be reading it.
Three things follow from that shape, and each is enforced by a test:
Because the rules travel with the data, an app can open a snapshot produced by an older build — or by a capability it no longer collects — and still render, diff, search, export, and classify it honestly. Real examples: Fixtures/snapshots/. Full reference: Documentation/SnapshotSchema.md.
Store review takes days. A confusing help section or a typo in onboarding should not have to wait that long, and every mature store app solves this with server-driven UI: the screen is described by data the app fetches rather than by code it ships.
Diffuse cannot fetch anything — local-first is a product guarantee and the Android app declares no INTERNET permission. So the runtime is built and the transport is not. Surfaces are described by JSON, validated, and rendered natively; the only source is the app bundle. Adding a publisher later is one new SurfaceSource and one line of wiring, with the renderer, validator, and every test untouched.
| A payload may | A payload may not |
|---|---|
| Supply text, ordering, and structure | Supply colours, fonts, or spacing |
| Name an action the host already implements | Describe behaviour, expressions, or scripts |
| Introduce a node type this build skips | Reach snapshots, the diff engine, or privacy handling |
| Gate itself to a minimum app version | Force a screen to render nothing |
Actions are names. A surface can ask for capture; it cannot say what capturing means. That indirection is what keeps a data channel from becoming an execution channel.
Every surface has a hand-written native fallback. A payload that is missing, unreadable, built for a newer schema, aimed at a newer app version, or composed entirely of unknown nodes renders nothing and the native UI shows instead — individual bad nodes are pruned while their siblings still render. Delete every payload and the apps are exactly what they ship with, which is what makes the feature additive rather than load-bearing.
This is ADR 0003 applied one level up: a snapshot already carries the schema needed to render a capability the build has never heard of. Details in ADR 0010 and ARCHITECTURE.md.
These are product decisions, recorded as ADRs, not missing features:
| Platform | What it is |
|---|---|
| macOS | The full workspace. Split view, menu bar extra, scheduled capture, repository watch list, developer-tool collectors. |
| iOS | A phone app: overview, timeline, compare, settings. The pair to compare is chosen on the compare screen itself. Background refresh. Home Screen and Lock Screen widgets. |
| iPadOS | A three-column analytical workspace (including portrait). Same collectors as iPhone, reported as iPadOS. |
| watchOS | Standalone. Glance UI and Δ N complications. No paired iPhone required. |
| Android | Native Kotlin and Jetpack Compose. Adaptive navigation, local capture and history, compare, search, privacy ledger, import/export, retention, and WorkManager scheduling. |
The four Apple apps are separate SwiftUI targets rather than one max-target with size-class branches. Android does not wrap or invoke them: it is a standalone Gradle project and native Android application. Details: Documentation/Apps.md and Android/README.md.
flowchart TB
Contract[(Schema-v1 JSON + golden fixtures)]
Swift[First-party Swift domain packages] <--> Contract
Kotlin[Kotlin domain engine] <--> Contract
Swift --> Mac[macOS]
Swift --> iPhone[iOS]
Swift --> iPad[iPadOS]
Swift --> Watch[watchOS]
Kotlin --> Android[Android]
Captured from the live apps on 20 Aug 2026, with the iPad set retaken on 22 Aug 2026 — simulators, the macOS debug build, and a Pixel 6 Android 14 emulator. These are not mockups. Browse every PNG in docs/screenshots — that directory is the complete, versioned gallery for all five apps.
| Overview | Snapshots |
|---|---|
![]() |
![]() |
| Compare | Settings |
![]() |
![]() |
| Snapshot detail | Search |
![]() |
![]() |
| Privacy ledger | Capabilities |
![]() |
![]() |
Landscape (navigation rail) and destructive confirmations: android-overview-landscape, android-compare-landscape, android-settings-landscape, android-delete-all-confirmation, android-import-picker, android-share-chooser.
| Overview | Snapshots |
|---|---|
![]() |
![]() |
| Compare | Settings |
![]() |
![]() |
| Privacy | Capabilities |
![]() |
![]() |
| Search | Snapshot detail |
![]() |
![]() |
Entity inspector: ios-entity-detail.
| Workspace | Privacy |
|---|---|
![]() |
![]() |
| Capabilities | Settings |
![]() |
![]() |
Also: search, snapshot detail, entity detail, change detail.
| Overview | Snapshots |
|---|---|
![]() |
![]() |
| Compare | Capabilities |
![]() |
![]() |
| Privacy | Search |
![]() |
![]() |
Also: snapshot detail, entity detail, name this snapshot.
| Glance | Settings |
|---|---|
![]() |
![]() |
| Snapshot | Change |
![]() |
![]() |
flowchart LR
APIs[Local device APIs] --> Store[(Private local store)]
Store --> Screen[On-device UI]
Store --> Policy{Chosen export policy}
Policy --> Redacted[Redacted JSON or Markdown]
Redacted --> Share[System share sheet / chosen file]
flowchart TD
Value[Observed property] --> Class{Classification}
Class -->|public| Exportable[May export under any policy]
Class -->|local| Standard[Standard export; stripped on strict]
Class -->|sensitive| Full[Only full-detail export]
Class -->|restricted| Never[Never leaves the device]
Diffuse does not scale a Mac inventory down to a Watch. Each registry lists only what that device can observe.
flowchart TB
Q[What changed on this device?] --> Mac[macOS: system, hardware, displays,<br/>network, apps, tools, git, processes]
Q --> Mobile[iPhone / iPad: device, battery,<br/>screen, container storage, path]
Q --> Watch[Watch: device, battery, system,<br/>container, network path]
Q --> Droid[Android: device, system, battery,<br/>display, data volume, connectivity]
The decide function is shared in spirit on every platform. The driver is native.
flowchart TD
Trigger[Manual, cadence, app open, wake] --> Enabled{Schedule on?}
Enabled -->|no, automatic| Idle[Idle]
Enabled -->|yes or manual| Floor{Inside 15-minute floor?}
Floor -->|yes, automatic| Idle
Floor -->|no| Capture[Capture]
Capture --> Skip{Automatic and unchanged?}
Skip -->|yes| Drop[Do not persist]
Skip -->|no| Save[Persist, then retain]
| Platform | Driver |
|---|---|
| macOS | Timer + unlock/wake |
| iOS / iPadOS | Capture on open + BGAppRefresh |
| watchOS | Watch refresh task |
| Android | WorkManager + due check on open |
Defaults: every four hours, 15-minute floor, skip-if-unchanged on automatic captures, 90 days / 1 GiB retention, newest always kept.
diffuse-dev is a client of the same domain engine the apps use. It imports no UI framework, and it does not touch your app library unless you point it at a path.
swift run diffuse-dev capabilities # what this machine can observe
swift run diffuse-dev snapshot today.json --label "before upgrade"
swift run diffuse-dev inspect today.json --verbose
swift run diffuse-dev diff before.json after.json --markdown --severity significant
swift run diffuse-dev validate today.json # exit 1 on a structural problem
swift run diffuse-dev privacy # the generated privacy ledger--fail-on-change exits 2 when a diff contains anything significant or higher, which makes the CLI usable as a drift check in someone else's pipeline:
swift run diffuse-dev snapshot now.json
swift run diffuse-dev diff baseline.json now.json --fail-on-change || echo "the machine moved"Argument parsing is hand-rolled — there is no swift-argument-parser (ADR 0004). Full reference: Documentation/CLI.md.
Start here on any machine:
make doctor # what can this machine build?
make help # every supported commandmake doctor reports the Apple, Android, and shared toolchains separately, so you learn up front which suites are available instead of discovering it when one fails halfway through.
The Apple apps require macOS, Xcode 16+ / Swift 6, XcodeGen, and SwiftFormat.
./Scripts/bootstrap.sh # generates Diffuse.xcodeproj (gitignored)
./Scripts/verify.sh # format, tests, SDK cross-check, unsigned Apple app buildsA healthy checkout prints Diffuse is healthy.
Running any of the five apps takes one command — no Xcode UI, no simulator UDID, no JDK setup:
make ios-run # DiffuseiOS on an iPhone simulator
make ipados-run # DiffuseiPadOS on an iPad simulator
make watch-run # DiffuseWatch on a watch simulator
make mac-run # DiffuseMac on this machine
make android-run # the Android app on a device or emulatorEach resolves and boots a simulator or checks for a connected device, builds, installs, and launches. make apple-devices and make android-devices list what is available; set DIFFUSE_SIMULATOR_ID to pin a specific one.
swift test --parallel # Swift Testing (~588 tests)
./Scripts/coverage.sh # llvm-cov HTML + lcov, gated at 90% (currently 92.4%)
swift run diffuse-dev --help # capture, inspect, diff, validate, fixtures, privacy
open Diffuse.xcodeproj # generated; do not check it inGit hooks (optional, Node 20+):
npm install # Husky + lint-staged; also installed by ./Scripts/bootstrap.shDocker / Dev Container cover docs, scripts, hooks, and the full Android build — not Xcode builds, which need macOS:
docker compose up -d
docker compose exec dev bash
make verify-devcontainer # build the image, assert its toolchain works
./Scripts/verify-devcontainer.sh --build # also build the Android app inside itThe image is pinned to linux/amd64 because Google ships the Android command-line tools and build-tools for Linux as x86_64 binaries only. On Apple silicon it runs under emulation and is noticeably slower. CI builds the same single architecture for the same reason — an arm64 image would build cleanly and then fail at aapt2 and d8, which is worse than not offering the architecture at all.
CI publishes this toolbox to the GitHub Container Registry on every push to a default branch, so you can pull it instead of building it:
docker run --rm -it ghcr.io/hoangsonww/diffuse-dev:latest bashA pull request builds the image but never publishes it. A fork's token cannot write packages, and pushing an image built from unreviewed code to a tag other people pull is not something a green check should do. Every pull request still proves the image builds and that the toolchain inside it works — JDK 17, adb, sdkmanager, Node with dependencies installed, and the shell tools the hooks depend on are each asserted in a running container, because an image that builds while missing a toolchain fails later and further from the cause.
CI then builds the actual Android app inside that image — assembleDebug, unit tests, and lint — and uploads the resulting APK. Checking that adb exists proves a binary is present; it does not prove the image can still build the app. An SDK bump, a Gradle bump, or a missing platform breaks the build while leaving every binary check green. android.yml covers the same build on a bare runner: that one guards the code, this one guards the container, and they fail for different reasons.
The Apple apps are absent from this image and always will be. Docker containers are Linux, Xcode is macOS-only with no Linux build, and Apple's licence forbids redistributing Xcode — any one of the three is fatal on its own.
Signing is not part of this repository. CI produces unsigned artifacts. Whoever ships Diffuse configures identities outside source control.
Requires Android SDK 36, and an emulator or device for instrumentation tests. A JDK is not a prerequisite — Android/gradle/gradle-daemon-jvm.properties pins daemon JVM criteria, so Gradle 8.13 downloads and runs on its own Adoptium JDK 17 for the host OS and architecture. ./gradlew works on a clean checkout whatever java is on PATH, including no JDK at all; the first build pays a one-time ~180 MB download.
make android-run # build, install, and launch
make android-test # unit tests with coverage
make android-lint
make android-release # unsigned release APK
make gradle ARGS="bundleRelease"Raw Gradle works too:
cd Android
./gradlew testDebugUnitTest
./gradlew jacocoDebugUnitTestReport
./gradlew jacocoDebugUnitTestCoverageVerification
./gradlew connectedDebugAndroidTest
./gradlew lintDebug assembleDebug bundleReleaseThe Android JVM suite (96 tests) exercises the pure Kotlin engine and reads the repository’s shared Fixtures/ directly. CI enforces at least 90% line coverage for com.diffuse.android.domain, currently 97.7%; the 15 emulator tests cover Android collectors, preferences, storage/service integration, navigation, dialogs, rotation, and adaptive layout. Compose UI and the Android framework collectors are measured there rather than inflating the JVM number with mocks.
| Concern | Apple | Android |
|---|---|---|
| Domain tests | Swift Testing | JUnit + coroutine test |
| Coverage | First-party llvm-cov, 90% floor — 92.4% | JaCoCo, 90% domain floor — 97.7% |
| UI/device tests | Unsigned simulator builds + live capture | Compose instrumentation on emulator |
| Cross-platform contract | Golden schema-v1 snapshots/diffs | The same fixtures decoded and diffed in Kotlin |
| Static checks | SwiftFormat + SDK cross-check | Android Lint + release shrinking |
| Shipping artifacts | Unsigned .app/archives | Unsigned release bundle; signing external |
flowchart LR
subgraph AppleVerify[Apple]
SF[SwiftFormat]
ST[swift test]
XC[SDK cross-check]
XB[Unsigned xcodebuild]
end
subgraph AndroidVerify[Android]
UT[Gradle unit tests]
JC[JaCoCo gate]
AL[Lint]
IT[Emulator instrumentation]
end
SF --> Healthy[Diffuse is healthy]
ST --> Healthy
XC --> Healthy
XB --> Healthy
UT --> Ship[Unsigned Android artifacts]
JC --> Ship
AL --> Ship
IT --> Ship
Two native families, one contract, and a deliberately short dependency list.
| Apple | Android | |
|---|---|---|
| Language | Swift 6, language mode .v6, complete concurrency | Kotlin 2.0.21 on JVM 17 |
| UI | SwiftUI, with AppKit / UIKit / WatchKit hosts | Jetpack Compose, Material 3 |
| Build | Swift Package Manager, XcodeGen, xcodebuild | Gradle 8.13, AGP 8.10.1 |
| Concurrency | Structured concurrency, actors, TaskGroup | Coroutines, Flow, StateFlow |
| Serialization | Codable via first-party SnapshotCoding | kotlinx.serialization |
| Background work | Timer, BGAppRefreshTask, WKApplicationRefreshBackgroundTask | WorkManager |
| Tests | Swift Testing | JUnit 4, Compose UI Test, Espresso |
| Coverage | llvm-cov | JaCoCo, 90% domain floor |
The Apple side links zero third-party packages (ADR 0004). Android's dependency graph is limited to Google's AndroidX and JetBrains' Kotlin libraries (ADR 0009). There is no database engine, no HTTP client, no analytics SDK, and no crash reporter in any app.
Every technology in the badge row above — and why it is here — is catalogued in Documentation/TechStack.md.
Human and agent contributors share one contract: AGENTS.md. Skills live in .agents/skills/. Claude Code, Codex, Cursor, Copilot, and Gemini each have a thin pointer (CLAUDE.md, .codex/, .cursor/rules/, .github/copilot-instructions.md, GEMINI.md).
Two native families, one data contract. The repository-wide map is ARCHITECTURE.md.
flowchart TD
AppleApps[macOS · iOS · iPadOS · watchOS] --> UI[DiffuseUI]
UI --> Core[DiffuseCore]
AppleApps --> Collectors[DiffuseCollectors]
Core --> Models[DiffuseModels]
Core --> Diff[DiffuseDiff]
Core --> Storage[DiffuseStorage]
Core --> Capabilities[DiffuseCapabilities]
Collectors --> Capabilities
AndroidUI[Android Compose UI] --> AndroidCore[Kotlin service + domain]
AndroidCore --> AndroidCollectors[Android collectors]
Models <--> Fixtures[(Schema-v1 fixtures)]
AndroidCore <--> Fixtures
sequenceDiagram
actor User
participant UI as Native app
participant Coordinator
participant Collectors
participant Store as On-device store
participant Engine as Diff engine
User->>UI: Take snapshot
UI->>Coordinator: Capture enabled capabilities
Coordinator->>Collectors: Collect concurrently with deadlines
Collectors-->>Coordinator: Sections + schemas + diagnostics
Coordinator->>Store: Atomic local save
User->>UI: Compare two snapshots
UI->>Store: Load selected pair
Store->>Engine: Base + target
Engine-->>UI: Deterministic changes and severity summary
User->>UI: Share report
UI-->>User: Classification-redacted export
Collector failure is isolated: one failed or timed-out capability becomes a section status and diagnostic while successful sections remain usable. Retention runs after save, and the newest snapshot is always protected.
flowchart LR
JSON[Pretty-printed snapshot JSON] --> Validate[Structural validation]
Validate --> Display[Generic UI from travelling schema]
Validate --> Compare[Schema-driven diff]
Validate --> Search[Library / change search]
Validate --> Export[Redacted export]
Validate --> Ledger[Generated privacy ledger]
Start with the repository-wide ARCHITECTURE.md, then use Documentation/ for the Swift engine, glossary, capability/collector guides, schema, diff, storage, privacy, apps, CLI, testing, repository layout, and ADRs.
| I want to… | Read |
|---|---|
| Understand the complete Swift + Kotlin system | Root architecture |
| Understand the Swift package layers | Swift architecture |
| Build or test Android | Android guide |
| Add something Diffuse can observe | Capability guide |
| Debug a collector | Collector guide |
| Change JSON shape or fixtures | Snapshot schema |
| Understand comparison | Diff engine |
| Find files on disk / retention | Storage |
| Know what is collected | Privacy |
| Work on Mac / iPhone / iPad / Watch / Android | Apps |
| Use diffuse-dev | CLI |
| Write tests or read coverage | Testing |
| Find a script or CI job | Repository |
| Decode a term | Glossary |
| See every technology used and why | Tech stack |
| Fix a build, test, or emulator failure | Troubleshooting |
| Cut a tag and ship artifacts | Releasing |
Machine-readable metadata is in CITATION.cff; GitHub renders it as a "Cite this repository" button.
@software{nguyen_diffuse,
author = {Nguyen, Son and {The Diffuse contributors}},
title = {Diffuse: local-first device history for Apple platforms and Android},
year = {2026},
version = {1.0.0},
license = {MIT},
url = {https://github.com/hoangsonww/Diffuse-Native-Apps}
}MIT. See LICENSE.
Diffuse links no third-party Swift code, so there is no bundled-license file to audit on the Apple side. The Android app depends only on AndroidX (Apache-2.0) and Kotlin/JetBrains libraries (Apache-2.0), declared in Android/app/build.gradle.kts.
| Back | FazBrowse Home | New Git URL |
{ "format": "diffuse.snapshot", "schemaVersion": 1, "snapshot": { "id": "sample-mac-baseline", "capturedAt": "2026-08-19T09:00:00Z", "platform": "macOS", "origin": "manual", "device": { "id": "…", "model": "Mac15,3", "systemName": "macOS", "systemVersion": "26.0.0" }, "metadata": { "appVersion": "1.0.0", "appliedRedaction": "none", "skippedCapabilities": [] }, "sections": [ { "capability": "system.info", "collector": "sample.system", "collectedAt": "2026-08-19T09:00:00Z", "duration": 0.012, "diagnostics": [], // The travelling schema: how to label, order, compare, rate, and classify. "schema": { "displayName": "System", "category": "system", "privacy": "local", "entityKinds": [{ "kind": "system", "properties": [ { "key": "os.name", "displayName": "Operating system", "comparison": { "exact": {} }, "severity": "critical", "privacy": "public", "unit": "none", "isPrimary": true }, { "key": "os.version", "displayName": "Version", "comparison": { "semanticVersion": {} }, "severity": "significant", "privacy": "public", "unit": "version", "isPrimary": true }, { "key": "uptime", "displayName": "Uptime", "comparison": { "ignored": {} }, "severity": "informational", "privacy": "public", "unit": "seconds", "isPrimary": false } ] }] }, // The observation itself. "entities": [{ "identity": { "kind": "system", "value": "primary" }, "displayName": "macOS", "properties": { "os.version": { "type": "version", "value": "26.0.0" }, "uptime": { "type": "duration", "value": 358720 } } }] } ] } }