diff --git a/.hermes/skills/devlog-architecture-harness/SKILL.md b/.hermes/skills/devlog-architecture-harness/SKILL.md index 2b65ef74..2da1b998 100644 --- a/.hermes/skills/devlog-architecture-harness/SKILL.md +++ b/.hermes/skills/devlog-architecture-harness/SKILL.md @@ -75,8 +75,15 @@ Ask the user before editing when: - A shared type is being moved only because multiple modules need access. - Firebase/Auth/Firestore/Functions/Messaging-specific logic would leave Infra. - WidgetCore would depend on Domain, Data, Infra, Persistence, Presentation, or App. +- DevLogWidget would depend on Domain, Infra, Persistence, Presentation, or App. +- DevLogWidget would use WidgetKit for anything beyond the app-side widget reload bridge. +- Widget sync ownership would move away from the preferred split: Data contracts and snapshot repositories, DevLogWidget app-side bridge/snapshot update orchestration, WidgetCore snapshot models/factories/store contracts, and WidgetExtension rendering. +- Persistence would gain widget snapshot generation, WidgetCore, WidgetKit reload, or DevLogWidget bridge ownership. - Presentation would depend on Data, Infra, Persistence, or App. - Data would gain concrete SDK or storage implementation details. +- Data or Presentation would expand platform SDK usage beyond the existing narrow cancellation-classification or notification-badge patterns. +- Infra would add any Domain dependency, source import, or SDK service contract coupling. +- A same-layer dependency would be injected outside a SwiftUI `View` file in `Application/DevLogPresentation`. - The Presentation `StorePattern` flow or reducer responsibility would change. - A compile fix requires relaxing the intended architecture. - The change is outside the requested issue or PR scope. @@ -118,6 +125,9 @@ After completion, report only: - Do not infer project-specific architecture policy from generic Clean Architecture rules when DevLog already has a concrete pattern. - Do not move domain entities to Core just because multiple modules need them. +- Do not inject same-layer dependencies except from SwiftUI `View` files in `Application/DevLogPresentation` into same-layer presentation objects for UI composition. +- Do not collapse the current DevLogWidget bridge back into Data, Persistence, App, or WidgetCore without explicit boundary approval. +- Route app-side widget bridge and snapshot update orchestration through DevLogWidget. - Do not hide architecture decisions inside build-fix wording. - Do not broaden a modularization task into unrelated Firestore, Messaging, or UI safety edits. - Do not mark work complete if the diff contains unrelated project-file or lockfile churn. diff --git a/.hermes/skills/devlog-architecture-harness/references/devlog-architecture-flow.md b/.hermes/skills/devlog-architecture-harness/references/devlog-architecture-flow.md index 17542331..ab13ca35 100644 --- a/.hermes/skills/devlog-architecture-harness/references/devlog-architecture-flow.md +++ b/.hermes/skills/devlog-architecture-harness/references/devlog-architecture-flow.md @@ -77,8 +77,9 @@ flowchart TD Domain["DevLogDomain\nEntities\nRepository protocols\nUse cases"] Data["DevLogData\nRepository implementations\nDTOs\nMappers\nService/store protocols"] Infra["DevLogInfra\nFirebase\nSocial login\nNetwork\nLink metadata\nMessaging"] - Persistence["DevLogPersistence\nUserDefaults\nImage store\nWidget snapshot persistence"] - Core["DevLogCore\nDI\nLogger\nShared value/query types\nWidget snapshot values"] + Persistence["DevLogPersistence\nUserDefaults\nImage store\nNon-widget app persistence"] + Widget["DevLogWidget\nApp-side widget bridge\nSync/session handlers\nSnapshot update orchestration\nWidgetKit reload bridge"] + Core["DevLogCore\nDI\nLogger\nShared value/query types\nLightweight widget values"] WidgetCore["DevLogWidgetCore\nWidget snapshot models\nFactories\nApp Group constants"] WidgetExtension["DevLogWidgetExtension\nWidgetKit UI\nProviders\nTimelines"] @@ -88,7 +89,9 @@ flowchart TD App --> Infra App --> Persistence App --> Core + App --> Widget App --> WidgetCore + App -.-> WidgetExtension Presentation --> Domain Presentation --> Core @@ -103,7 +106,10 @@ flowchart TD Persistence --> Data Persistence --> Core - Persistence --> WidgetCore + + Widget --> Data + Widget --> Core + Widget --> WidgetCore WidgetExtension --> WidgetCore WidgetCore --> Core @@ -113,16 +119,27 @@ flowchart TD | Layer | Owns | Allowed direction | Ask before | | --- | --- | --- | --- | -| `DevLogCore` | DI primitives, logger, shared value/query types, widget snapshot values | No DevLog layer dependency | Moving domain entities into Core | +| `DevLogCore` | DI primitives, logger, shared value/query types, display options, activity kinds, lightweight widget bridge values | No DevLog layer dependency | Moving domain entities into Core | | `DevLogDomain` | entities, repository protocols, use cases | Core only | Adding Data, Infra, Persistence, Presentation, App, Widget UI, or SDK dependency | -| `DevLogData` | repository implementations, DTOs, mappers, data protocols | Domain, Core | Adding concrete Firebase, WidgetKit, storage, or platform implementation details | -| `DevLogInfra` | Firebase, social login, network, metadata, messaging implementations | Data, Core | Moving SDK-specific behavior out of Infra | -| `DevLogPersistence` | local stores, image cache, widget snapshot persistence | Data, Core, WidgetCore | Moving domain logic or remote SDK behavior into Persistence | -| `DevLogPresentation` | UI, view models, coordinators, presentation state | Domain, Core | Adding Data, Infra, Persistence, or App dependency | -| `DevLogApp` | composition root, lifecycle, assembler wiring | Concrete app layers | Moving feature logic into App | -| `DevLogWidgetCore` | widget data contracts and pure snapshot logic | Core | Adding Domain, Data, Infra, Persistence, Presentation, or App dependency | +| `DevLogData` | repository implementations, DTOs, mappers, data protocols, widget repository/updater/sync contracts | Domain, Core | Adding concrete Firebase, GoogleSignIn, WidgetKit, storage, WidgetCore snapshot model/factory usage, or platform implementation details; moving concrete widget handlers into Data | +| `DevLogInfra` | Firebase, social login, network, metadata, messaging implementations | Data, Core | Moving SDK-specific behavior out of Infra; adding any Domain dependency, source import, or SDK service contract coupling | +| `DevLogPersistence` | local stores, image cache, non-widget app persistence | Data, Core | Adding WidgetCore, WidgetKit reload, DevLogWidget, widget snapshot generation, or widget bridge ownership | +| `DevLogPresentation` | UI, view models, coordinators, presentation state, narrow presentation-scoped platform side effects | Domain, Core | Adding Data, Infra, Persistence, or App dependency; expanding platform service ownership beyond UI-side effects | +| `DevLogWidget` | app-side widget bridge, sync bus implementation, sync/session handlers, snapshot generation/persistence orchestration, WidgetKit reload bridge, widget assembler | Data, Core, WidgetCore | Adding Domain, Infra, Persistence, Presentation, or App dependency | +| `DevLogApp` | composition root, lifecycle, assembler wiring, app target ownership for widget extension embedding | Concrete app layers | Moving feature logic into App | +| `DevLogWidgetCore` | widget snapshot models, factories, app-group keys/defaults store, deep links, pure snapshot logic | Core | Adding Domain, Data, Infra, Persistence, Presentation, App, or DevLogWidget dependency | | `DevLogWidgetExtension` | WidgetKit rendering and timeline plumbing | WidgetCore | Calling app/domain services directly | +## Layer-internal dependency injection + +Do not inject dependencies between types that belong to the same layer. + +This rule covers initializer injection, stored-property injection, environment injection, and resolving same-layer types through `DIContainer`. + +The only allowed exception is a SwiftUI `View` file in `Application/DevLogPresentation` receiving same-layer presentation objects such as a ViewModel, Coordinator, or Store for UI composition. + +That exception does not apply to non-View files in Presentation, and does not apply to Core, Domain, Data, Infra, Persistence, Widget, App, WidgetCore, WidgetExtension, or Firebase functions. + ## Presentation StorePattern flow ```mermaid @@ -158,8 +175,9 @@ flowchart TD CoreDomain{"Core vs Domain ownership?"} Shared{"Moved only because shared?"} NewDependency{"New module dependency?"} + SameLayerDI{"Same-layer dependency injection?"} ExternalSDK{"External SDK crosses layer?"} - WidgetBoundary{"WidgetCore sees app/domain/data?"} + WidgetBoundary{"Widget sync ownership or WidgetCore boundary changes?"} BuildShortcut{"Build fix relaxes boundary?"} ScopeDrift{"Outside current task scope?"} Ask["Ask user before editing"] @@ -171,7 +189,10 @@ flowchart TD Shared -->|Yes| Ask Shared -->|No| NewDependency NewDependency -->|Yes| Ask - NewDependency -->|No| ExternalSDK + NewDependency -->|No| SameLayerDI + SameLayerDI -->|Presentation View file| ExternalSDK + SameLayerDI -->|No| ExternalSDK + SameLayerDI -->|Other| Ask ExternalSDK -->|Yes| Ask ExternalSDK -->|No| WidgetBoundary WidgetBoundary -->|Yes| Ask @@ -216,23 +237,34 @@ Use this flow before introducing or moving imports such as Firebase, GoogleSignI flowchart TD Import["External framework import"] Firebase{"Firebase/Auth/Firestore/Functions/Messaging?"} - SocialLogin{"GoogleSignIn or AuthenticationServices login implementation?"} + SocialLogin{"GoogleSignIn or AuthenticationServices?"} + SocialLoginClassification{"Existing presentation/data cancellation/error classification?"} NetworkMeta{"Network or LinkPresentation implementation?"} + UserNotifications{"UserNotifications?"} WidgetKit{"WidgetKit?"} Infra["Prefer DevLogInfra"] - Persistence["Allow only for widget snapshot update/persistence if already established"] + ErrorClassification["Keep narrow in DevLogData or DevLogPresentation only when matching the existing cancellation-classification pattern"] + PresentationBadge["Allow in DevLogPresentation only for established badge/UI side effects"] + Widget["Allow in DevLogWidget for app-side snapshot update/reload orchestration"] WidgetExtension["Allow in DevLogWidgetExtension rendering/timeline code"] Ask["Ask user before crossing layer"] Import --> Firebase Firebase -->|Yes| Infra Firebase -->|No| SocialLogin - SocialLogin -->|Yes| Infra + SocialLogin -->|Login implementation| Infra + SocialLogin -->|Presentation/data error classification| SocialLoginClassification + SocialLoginClassification -->|Matches existing pattern| ErrorClassification + SocialLoginClassification -->|New or broader behavior| Ask SocialLogin -->|No| NetworkMeta NetworkMeta -->|Yes| Infra - NetworkMeta -->|No| WidgetKit + NetworkMeta -->|No| UserNotifications + UserNotifications -->|Badge/UI side effect| PresentationBadge + UserNotifications -->|Push or messaging service| Infra + UserNotifications -->|Other| Ask + UserNotifications -->|No| WidgetKit WidgetKit -->|Widget UI| WidgetExtension - WidgetKit -->|Snapshot update already established| Persistence + WidgetKit -->|App-side snapshot update/reload| Widget WidgetKit -->|Other| Ask ``` @@ -240,16 +272,24 @@ flowchart TD ```mermaid flowchart LR - App["App runtime\nDomain/Data/Infra/Persistence"] - Snapshot["Snapshot generation\nPersistence + WidgetCore"] + App["App runtime\nsession and mutation events"] + WidgetBridge["DevLogWidget\nsync bus implementation\nsync/session handlers"] + DataContracts["DevLogData\nwidget repository/updater contracts"] + SnapshotInputs["DevLogData\nsnapshot input repository"] + Snapshot["DevLogWidget\nsnapshot generation/persistence\nWidgetKit reload bridge"] + WidgetModels["DevLogWidgetCore\nsnapshot models/factories/store contracts"] AppGroup["App Group storage\nShared defaults"] - WidgetCore["WidgetCore\nSnapshot models\nFactories"] WidgetExtension["Widget extension\nWidgetKit UI"] - App --> Snapshot - Snapshot --> AppGroup - AppGroup --> WidgetCore - WidgetCore --> WidgetExtension + App --> WidgetBridge + WidgetBridge --> DataContracts + DataContracts --> SnapshotInputs + DataContracts --> Snapshot + SnapshotInputs --> WidgetBridge + Snapshot --> WidgetModels + WidgetModels --> AppGroup + AppGroup --> WidgetModels + WidgetModels --> WidgetExtension ``` Widget UI should consume snapshot data. It should not fetch app services or domain repositories directly. diff --git a/.hermes/skills/devlog-architecture-harness/references/devlog-workflow-rules.md b/.hermes/skills/devlog-architecture-harness/references/devlog-workflow-rules.md index 5ab98de6..12e8a78a 100644 --- a/.hermes/skills/devlog-architecture-harness/references/devlog-workflow-rules.md +++ b/.hermes/skills/devlog-architecture-harness/references/devlog-workflow-rules.md @@ -29,6 +29,7 @@ This reference holds DevLog-specific working rules that should live with the pro - Do not force a single `objectVersion` across projects. Treat Xcode's actual save output as the source of truth. - For synchronized-root cleanup, verify on copied files or a narrowed rule set before touching real project files. - When changing project structure, update the Tuist manifest first and treat generated Xcode project churn as disposable output. +- Do not promote a manifest-only target dependency to an allowed architecture direction. Check source imports and ownership before updating the layer map. ## PR and review handling @@ -56,20 +57,30 @@ This reference holds DevLog-specific working rules that should live with the pro - Keep project-file, lockfile, and code changes separated when the task scope requires clean review. - Do not broaden architecture work into unrelated Firestore, Messaging, UI, or safety edits. +## Layer-internal dependency injection + +- Do not inject dependencies between types that belong to the same layer. +- This includes initializer injection, stored-property injection, environment injection, and resolving same-layer types through `DIContainer`. +- The only allowed exception is a SwiftUI `View` file in `Application/DevLogPresentation` receiving same-layer presentation objects such as a ViewModel, Coordinator, or Store for UI composition. +- The exception does not apply to non-View files in Presentation, and does not apply to Core, Domain, Data, Infra, Persistence, Widget, App, WidgetCore, WidgetExtension, or Firebase functions. + ## Data, Domain, and Infra boundary - Do not move domain entities to Core only because multiple modules need them. - Keep protocol location and implementation layer distinct when explaining or changing boundaries. - If a Data protocol is implemented by Infra, every type in that protocol signature must be visible to Infra. +- `DevLogInfra` should depend on Data and Core, not Domain. Do not treat a manifest-only Domain target dependency as architecture permission. - Prefer a Data-side boundary value plus repository mapping when Infra should not import Domain. - For example, keep the app-facing Domain query separate from an Infra-facing Data query when that avoids Domain coupling in service protocols. - Firebase-specific error detection belongs in Infra; Data should handle domain-level errors after mapping. +- Data and Presentation currently keep narrow social-login cancellation classification in `DataLayerError` and `Error+SocialLogin`. Do not expand that into concrete login implementation or broader SDK ownership without explicit approval. ## Presentation StorePattern - Preserve the existing `StorePattern` shape: `@MainActor`, `State`, `Action`, `SideEffect`, `send -> reduce -> run`. - Reducers compute state and return side effects. - I/O belongs in `run` or injected services. +- Presentation currently owns narrow notification badge side effects through `UserNotifications`. Do not expand that into push service or messaging ownership. - Do not leave reducer-era helper methods behind after moving work into `run`. - Before adding task cancellation or async wrappers, inspect whether the underlying operation is actually async. @@ -77,7 +88,10 @@ This reference holds DevLog-specific working rules that should live with the pro - Widget UI should consume snapshot data, not app/domain services. - `DevLogWidgetCore` should stay free of Domain, Data, Infra, Persistence, Presentation, and App dependencies unless the user explicitly approves a boundary change. -- Prefer an app-driven snapshot flow: app/runtime data fetch, snapshot generation, App Group storage, WidgetCore model/factory, WidgetExtension rendering. +- `DevLogWidget` owns the app-side widget bridge: sync event bus implementation, sync event handlers, session sync handler, auth-session sync provider, snapshot generation/persistence orchestration, WidgetKit reload bridge, and `WidgetAssembler`. +- `DevLogData` owns widget-related contracts and repository implementations, including `WidgetSyncEventBus`, `WidgetSnapshotUpdater`, and `WidgetTodoSnapshotRepository`. Data should not own concrete widget handlers, WidgetCore snapshot model/factory usage, or WidgetKit reload behavior. +- `DevLogPersistence` owns local persistence, user defaults, image store, and non-widget app persistence. +- Prefer an app-driven snapshot flow: app/runtime event, DevLogWidget sync handler, Data snapshot input fetch, DevLogWidget snapshot update, App Group storage through WidgetCore contracts, WidgetExtension rendering. - `WidgetTodoSnapshot` is a lightweight snapshot value, not a full domain `Todo`. - Do not make `Todo.number` or `WidgetTodoSnapshot.number` non-optional without a separate saved-vs-draft model decision. - If a widget sync flow needs one timestamp for multiple snapshots, capture `Date()` once and pass it through to avoid midnight or quarter-boundary drift. diff --git a/AGENTS.md b/AGENTS.md index f7ddad26..a2b97e03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,17 +41,25 @@ Treat this repository as a Tuist-generated, workspace-based modular iOS app. The ### Current layer map -- `Application/DevLogCore`: shared app primitives such as DI, logging, query/value types, display options, and widget snapshot value types. Core is shared, but shared access alone is not enough reason to move domain entities into Core. +- `Application/DevLogCore`: shared app primitives such as DI, logging, query/value types, display options, activity kinds, and lightweight widget bridge values such as `WidgetTodoSnapshot`. Core is shared, but shared access alone is not enough reason to move domain entities into Core. - `Application/DevLogDomain`: entities, repository protocols, use case protocols, and use case implementations. Domain may depend on Core. Domain must not depend on Data, Infra, Persistence, Presentation, App, Widget extension UI, Firebase SDKs, or storage implementations. -- `Application/DevLogData`: repository implementations, DTOs, mappers, data-layer protocols for external services/stores, and widget sync coordination. Data may depend on Domain and Core. Data should not gain direct Firebase, GoogleSignIn, WidgetKit, or concrete storage implementation details unless the user explicitly approves the boundary change. -- `Application/DevLogInfra`: Firebase, social login, network, link metadata, messaging, and platform service implementations. Infra may depend on Data and Core. Firebase/Auth/Firestore/Functions/Messaging-specific behavior belongs here unless the user approves another boundary. -- `Application/DevLogPersistence`: local persistence, user defaults, image store, and widget snapshot persistence/updating. Persistence may depend on Data, Core, and WidgetCore when needed for snapshot persistence. -- `Application/DevLogPresentation`: SwiftUI views, view models, coordinators, UI state structures, and presentation-only helpers. Presentation may depend on Domain and Core. It must not depend on Data, Infra, Persistence, or App. -- `Application/DevLogApp`: composition root, app lifecycle, app delegate, app-level routing, and assembler wiring. App may import concrete layers to assemble the dependency graph. -- `Widget/DevLogWidgetCore`: widget snapshot models, factories, keys, app-group constants, and widget-only pure helpers. WidgetCore may depend on Core. It must not depend on Domain, Data, Infra, Persistence, Presentation, or App without explicit user approval. +- `Application/DevLogData`: repository implementations, DTOs, mappers, data-layer protocols for external services/stores, widget snapshot repository implementations, and widget sync/updater contracts. Data may depend on Domain and Core. Data should not own concrete widget sync handlers, event bus implementations, WidgetCore snapshot model/factory usage, or WidgetKit reload behavior, and should not gain direct Firebase, GoogleSignIn, WidgetKit, or concrete storage implementation details unless the user explicitly approves the boundary change. +- `Application/DevLogInfra`: Firebase, social login, network, link metadata, messaging, and platform service implementations. Infra may depend on Data and Core. Infra must not depend on Domain even if a manifest-only target dependency exists. Firebase/Auth/Firestore/Functions/Messaging-specific behavior belongs here unless the user approves another boundary. +- `Application/DevLogPersistence`: local persistence, user defaults, image store, and non-widget app persistence. Persistence may depend on Data and Core. Widget snapshot generation, widget snapshot persistence orchestration, and WidgetKit reload behavior belong to DevLogWidget. +- `Application/DevLogPresentation`: SwiftUI views, view models, coordinators, UI state structures, presentation-only helpers, and narrow presentation-scoped platform side effects such as notification badge updates. Presentation may depend on Domain and Core. It must not depend on Data, Infra, Persistence, or App. +- `Application/DevLogWidget`: app-side bridge between the app runtime and widget system, widget sync event bus implementation, widget sync/session handlers, auth-session sync provider, widget snapshot generation/persistence orchestration, WidgetKit reload bridge, and `WidgetAssembler`. DevLogWidget may depend on Data, Core, and WidgetCore. It must not depend on Domain, Infra, Persistence, Presentation, or App without explicit user approval. +- `Application/DevLogApp`: composition root, app lifecycle, app delegate, app-level routing, assembler wiring, and app target ownership for widget extension embedding. App may import concrete layers to assemble the dependency graph. +- `Widget/DevLogWidgetCore`: widget snapshot models, factories, keys, app-group constants/defaults store, deep links, and widget-only pure helpers. WidgetCore may depend on Core. It must not depend on Domain, Data, Infra, Persistence, Presentation, App, or DevLogWidget without explicit user approval. - `Widget/DevLogWidgetExtension`: WidgetKit UI, widget providers, entries, timelines, and extension resources. It should consume WidgetCore outputs rather than app/domain services directly. - `Firebase/functions`: TypeScript Cloud Functions. Deploy updated functions one by one separately. +### Layer-internal dependency injection + +- Do not inject dependencies between types that belong to the same layer. +- This applies to initializer injection, stored-property injection, environment injection, and resolving same-layer types through `DIContainer`. +- The only allowed exception is a SwiftUI `View` file in `Application/DevLogPresentation` receiving same-layer presentation objects such as a ViewModel, Coordinator, or Store for UI composition. +- The exception does not apply to non-View files in Presentation, and does not apply to Core, Domain, Data, Infra, Persistence, Widget, App, WidgetCore, WidgetExtension, or Firebase functions. + ### StorePattern flow - Preserve the existing Presentation `StorePattern`. @@ -71,6 +79,13 @@ Ask the user before editing when any of these are true: - Firebase, GoogleSignIn, AuthenticationServices, UserNotifications, LinkPresentation, Network, WidgetKit, or storage implementation details would move to another layer. - A repository protocol, service protocol, assembler, or DI ownership boundary would change. - WidgetCore would start depending on app/domain/data implementation concepts. +- DevLogWidget would start depending on Domain, Infra, Persistence, Presentation, or App. +- DevLogWidget would use WidgetKit for anything beyond the app-side widget reload bridge. +- Widget sync ownership would move away from the preferred split: Data contracts and snapshot repositories, DevLogWidget app-side bridge/snapshot update orchestration, WidgetCore snapshot models/factories/store contracts, and WidgetExtension rendering. +- Persistence would gain widget snapshot generation, WidgetCore, WidgetKit reload, or DevLogWidget bridge ownership. +- Data or Presentation would expand platform SDK usage beyond the existing narrow cancellation-classification or notification-badge patterns. +- Infra would add any Domain dependency, source import, or SDK service contract coupling. +- A same-layer dependency would be injected outside a SwiftUI `View` file in `Application/DevLogPresentation`. - The requested change suggests cleanup outside the current issue or PR scope. ### Safe mechanical changes diff --git a/Application/DevLogInfra/Project.swift b/Application/DevLogInfra/Project.swift index a9cf33e9..8caad8b1 100644 --- a/Application/DevLogInfra/Project.swift +++ b/Application/DevLogInfra/Project.swift @@ -10,7 +10,6 @@ let project = Project.devlogFramework( packages: DevLogPackages.infraPackages, dependencies: [ .project(target: "DevLogData", path: "../DevLogData"), - .project(target: "DevLogDomain", path: "../DevLogDomain"), .project(target: "DevLogCore", path: "../DevLogCore"), ] + DevLogPackages.infraPackageDependencies, hasTests: true diff --git a/docs/graph.png b/docs/graph.png index 2371b92b..6abb2431 100644 Binary files a/docs/graph.png and b/docs/graph.png differ