diff --git a/AGENTS.md b/AGENTS.md index 5cc7679..0366cb3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ Read only the guides relevant to the task: - [SwiftLint](AgentGuidelines/Guidelines/Swift/SwiftLint.md) - [Unit and integration testing](AgentGuidelines/Guidelines/Testing/UnitTesting.md) - [Documentation](AgentGuidelines/Guidelines/Documentation.md) +- [Logging](AgentGuidelines/Guidelines/Logging.md) - [Packages](AgentGuidelines/Guidelines/Packages.md) - [CI/CD](AgentGuidelines/Guidelines/CICD.md) - [Git repositories and SSH-first cloning](AgentGuidelines/Guidelines/Git/Repositories.md) @@ -39,3 +40,4 @@ Redux, SwiftUI, and application-localization guidance do not apply to the packag - Host applications own mapping from their domain identifiers and outcomes into `PKEvent`. - Preserve compiler-synthesized value semantics and serialization when evolving public models. - Update tests, DocC, README examples, and release notes when public behavior changes. +- Use logging subsystem `com.thatfactory.progressionkit`, category `progression`, and canonical package emoji `๐Ÿ“ˆ`. diff --git a/AgentGuidelines/AGENTS.md b/AgentGuidelines/AGENTS.md index aef307d..f3cc3d6 100644 --- a/AgentGuidelines/AGENTS.md +++ b/AgentGuidelines/AGENTS.md @@ -10,6 +10,7 @@ This public repository is the versioned source of truth for reusable ThatFactory - Distill durable policy from Xcode-provided skills; do not copy exported Apple skills into this repository. - Do not include private company information, credentials, personal absolute paths, or consumer-specific implementation details. - When shared and consumer guidance differ, the consumer's nearest applicable `AGENTS.md` is the explicit specialization. +- Before changing this repository, verify that the consumer's checked-in guidelines version is current where applicable. ## Documentation changes @@ -19,6 +20,21 @@ This public repository is the versioned source of truth for reusable ThatFactory - Use relative Markdown links inside this repository. - Update `README.md` when adding, moving, or removing a guide. - Update `CHANGELOG.md` and `VERSION` for a release. +- When releasing a new version, update the version in both the README installation command and the README consumer-update command. Keep both commands aligned with the new release, for example: + + ```sh + git subtree add \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + \ + --squash + + git subtree pull \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + \ + --squash + ``` ## Validation diff --git a/AgentGuidelines/CHANGELOG.md b/AgentGuidelines/CHANGELOG.md index 75a67e4..faa1a6b 100644 --- a/AgentGuidelines/CHANGELOG.md +++ b/AgentGuidelines/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to this project are documented in this file. +## [0.0.7] - 2026-07-23 + +### Added + +- Shared logging ownership, subsystem, package emoji, message design, privacy, testing, and filtering guidance. +- Logging pointers for application development, Swift packages, and consumer instruction templates. + +## [0.0.6] - 2026-07-22 + +### Added + +- Generic Redux store contracts, state/action, service-boundary, projection, and middleware guidance. +- Generic GitHub Actions workflow, self-hosted runner, build strategy, and failure-investigation guidance. +- Shared documentation conventions and test-tag/mock guidance. + +## [0.0.5] - 2026-07-21 + +### Added + +- Default DocC documentation and GitHub Pages publishing guidance for Swift packages. + +## [0.0.4] - 2026-07-21 + +### Added + +- Development guidance for reusability-first design and checking the latest shared-guidelines version before project work. + +### Changed + +- Require an approved pull request before releasing `agent-guidelines` or any consumer package. + ## [0.0.3] - 2026-07-21 ### Added diff --git a/AgentGuidelines/Guidelines/Architecture/Redux.md b/AgentGuidelines/Guidelines/Architecture/Redux.md index 77b20f6..56ab6f3 100644 --- a/AgentGuidelines/Guidelines/Architecture/Redux.md +++ b/AgentGuidelines/Guidelines/Architecture/Redux.md @@ -39,6 +39,28 @@ Use this guide for applications that explicitly adopt the ThatFactory Redux arch The store reduces the original action first, then awaits middleware and sequentially dispatches returned follow-up actions. Keep ordering observable and deterministic. Do not start unstructured work inside reducers or hide state changes inside services. +## Store + +Use one observable store as the source of truth and inject it at the application root. A store implementation may expose aliases like these: + +```swift +typealias AppStore = Store +typealias StateType = Equatable & Codable +typealias ActionType = Equatable +typealias Reducer = (State, Action) -> State +typealias Middleware = (State, Action) async -> Action? +``` + +Dispatch is asynchronous and ordered: + +1. Reduce the original action. +2. Capture the resulting state. +3. Await each registered middleware with that state and action. +4. Collect returned actions. +5. Dispatch follow-up actions sequentially. + +Use only `await store.dispatch(_:)`. Do not add a fire-and-forget dispatch API. + ## Canonical physical folders These are filesystem folders, not Xcode groups. New single-application repositories use this structure by default: @@ -118,6 +140,8 @@ Put the root state and domain sub-states in `Redux/State/`. Prefer focused value State stores durable facts. Avoid storing values that are cheap, deterministic derivations unless caching is an explicit measured requirement. +Sub-states should conform to `Equatable` and `Codable`; add `Sendable` when their values and concurrency boundaries require it. Keep root state and root actions for genuine cross-domain behavior. Keep domain action cases descriptive of intent or outcomes and route them through the root action. + ### Reducer Put reducer functions in `Redux/Reducer/`. A reducer receives state and an action and returns new state. It must not: @@ -135,7 +159,7 @@ Use the smallest state and action inputs that correctly express the transition. Put middleware in `Redux/Middleware/`. Middleware may call injected services and return a follow-up action. It must not mutate store state directly. -Inject services, clocks, identifier generators, and providers through parameters so middleware tests remain deterministic. Register middleware in one root composition file such as `AppMiddlewares.swift`. +Inject services, providers, managers, clocks, and identifier generators through parameters so middleware tests remain deterministic. Register middleware in one root composition file such as `AppMiddlewares.swift`. Reducers own every state mutation. Create a feature subfolder when a domain has multiple middleware files: @@ -158,6 +182,8 @@ Put focused external-boundary abstractions in `Services//`. Services Prefer a protocol or otherwise injectable contract when a service must be replaced in tests. Keep transport-specific details behind the service boundary. +Views dispatch actions; middleware calls services. Views never call a service directly for Redux-owned behavior. + ### Tools Put genuinely cross-cutting implementation utilities in `Tools/`. This is not a miscellaneous folder. Feature-only formatters, helpers, constants, or factories stay beside that feature. Promote them to `Tools/` only after they have a clear cross-feature role. @@ -177,6 +203,8 @@ View/Account/ If a projection exists only to render one screen, it is view-layer code even when its input is `AppState`. +Projection tests mirror the production view path under the test target. + ### Resources Put catalogs, assets, preview assets, configuration resources, and test plans in `Resources/` or the concrete resource folders declared locally. Production targets must not depend on test fixtures. diff --git a/AgentGuidelines/Guidelines/CICD.md b/AgentGuidelines/Guidelines/CICD.md index a66886e..c420331 100644 --- a/AgentGuidelines/Guidelines/CICD.md +++ b/AgentGuidelines/Guidelines/CICD.md @@ -12,7 +12,15 @@ - Do not place secrets in workflow files, logs, fixtures, or command arguments that may be echoed. - Keep release workflows separate from pull-request validation when their permissions differ. -## Pull-request CI +## `ci-pr.yml` + +Projects using GitHub Actions should keep pull-request validation in `.github/workflows/ci-pr.yml`, triggered by `pull_request` events for `opened`, `synchronize`, and `reopened`. + +Use GitHub-hosted runners for jobs that can run on the hosted operating system and toolchain. When a job uses a self-hosted runner, document and select it through the repository's `Runner labels:` rather than hard-coding a machine name in shared guidance. + +### Runner labels: + +When a workflow uses self-hosted runners, document the labels required by each job in this section of the consumer's CI/CD guide. Always include `self-hosted` and add only stable capability or environment labels needed to select the runner, such as an operating system, architecture, toolchain, or signing capability. Keep machine names and changing fleet details out of shared guidance. A typical Swift package validates: @@ -24,14 +32,27 @@ A typical Swift package validates: An Xcode application validates its declared scheme and test plan. Use the same project/workspace, configuration, and platform assumptions documented for local development. -## Investigation +Xcode projects and Swift packages must run on self-hosted macOS runners with the required Xcode, Swift toolchains, simulators, certificates, and signing environment. Do not use `macos-latest` for those jobs. For Xcode projects, test with `xcodebuild test` and explicit simulators, then validate compilation with `xcodebuild build CODE_SIGNING_ALLOWED=NO` across the supported platforms. For Swift packages, use Swift Package Manager commands such as `swift test` and `swift build`; packages do not require simulator selection, but may require the self-hosted signing environment for packaging or collection workflows. Generic jobs that do not require Apple tooling may use GitHub-hosted Linux or other suitable runners. CI validates tests and compile health, not app-store distribution. + +## `ci.yml` + +Validation of merges to `main` should live in `.github/workflows/ci.yml`, triggered by `push` on `main`. Use the same build, test, lint, and platform coverage as pull-request validation unless the repository documents a deliberate difference. + +## Failure investigation + +1. Use GitHub MCP connector tools to inspect check runs and logs for the failing commit or pull request. +2. Use `gh` for fast local triage when needed. +3. Reproduce locally with the exact build or test command shown in the failing job logs. + +Useful commands: + +```bash +gh run list --limit 10 +gh run view +gh run view --log +``` -1. Identify the first meaningful failing step rather than treating later cancellations as independent failures. -2. Reproduce locally with the closest supported toolchain when practical. -3. Separate infrastructure or dependency-resolution failures from code failures. -4. Fix the root cause in the narrowest appropriate layer. -5. Re-run the affected local validation before relying on remote CI. -6. Update durable CI documentation when the workflow or investigation process changes. +Distinguish compiler errors from lint violations, test failures from simulator or runtime infrastructure failures, and single-job failures from cross-platform matrix failures. Identify the first meaningful failing step, fix the narrowest root cause, and re-run affected validation. ## Releases diff --git a/AgentGuidelines/Guidelines/Development.md b/AgentGuidelines/Guidelines/Development.md new file mode 100644 index 0000000..8ab764d --- /dev/null +++ b/AgentGuidelines/Guidelines/Development.md @@ -0,0 +1,13 @@ +# Development + +## Reusability first + +When developing a new feature or responding to a feature request, consider shared code first. If the code fits an existing package, suggest extending that package instead of adding the implementation directly to an application. Also consider whether the change belongs in a new Swift package, even when that package does not exist yet. Prefer reusable, focused package APIs when they can serve more than one consumer. + +## Guidelines version + +Before changing a project, verify that it uses the latest released version of `agent-guidelines`. Check the project's `AgentGuidelines/VERSION` against the latest release, update the subtree or equivalent when it is behind, and read the updated applicable guides before starting implementation. This check is manual and must be performed at the beginning of each project task. + +## Logging + +Applications own their orchestration, lifecycle, and product-domain diagnostics. Follow the shared [logging guide](Logging.md) and rely on each dependency to log its own implementation. Do not duplicate or reformat package-internal operations in the application log. diff --git a/AgentGuidelines/Guidelines/Documentation.md b/AgentGuidelines/Guidelines/Documentation.md index b988361..2a0d327 100644 --- a/AgentGuidelines/Guidelines/Documentation.md +++ b/AgentGuidelines/Guidelines/Documentation.md @@ -1,5 +1,9 @@ # Documentation +- Use PascalCase Markdown filenames without spaces. +- Keep the folder flat until one topic genuinely requires several files. +- Prefer current implementation over speculative future design; label known gaps explicitly. + ## Code-level documentation - Document structs, classes, enums, protocols, actors, and other significant types with focused `///` DocC comments. diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/AgentGuidelines/Guidelines/Git/Repositories.md index a85949a..bff7933 100644 --- a/AgentGuidelines/Guidelines/Git/Repositories.md +++ b/AgentGuidelines/Guidelines/Git/Repositories.md @@ -20,3 +20,23 @@ git clone git@github.com:/.git ``` HTTPS remains appropriate for deliberately read-only retrieval, ephemeral automation, or environments where SSH credentials are unavailable. A public Git subtree remote may also remain HTTPS because consumers fetch tagged content without pushing to the guideline repository. + +## GitHub CLI authentication recovery + +Treat a reported invalid `GITHUB_TOKEN` as potentially transient or environment-specific. Do not abandon the `gh` CLI or switch protocols solely because one Codex shell reports that token as invalid. + +When `gh` authentication appears inconsistent: + +1. Retry `gh auth status` in a fresh shell. +2. If the user can run commands locally, ask them to confirm `gh auth status` and share only the redacted result; never request or print the token itself. +3. Retry the original `gh` command after authentication is confirmed. Preserve the CLI workflow for repository inspection, Actions logs, and pull-request operations. +4. If an injected environment variable is shadowing the stored GitHub CLI credential, compare the credential-backed check without exposing secrets: + + ```sh + env -u GITHUB_TOKEN gh auth status + ``` + + If that succeeds, use the authenticated CLI session for the task or refresh it with `gh auth refresh` as appropriate. Do not copy a token into shell history, command arguments, files, or chat. +5. Use SSH for Git transport only when the CLI remains unavailable after retry and the operation is specifically a Git fetch, commit, or push. Continue using `gh` for GitHub API operations whenever it is working. + +An environment mismatch is not evidence that the user's GitHub account or token is invalid. Record the failed command and exact non-secret error, retry after the authentication check, and report the blocker only after repeated attempts fail. diff --git a/AgentGuidelines/Guidelines/GitHub/PullRequests.md b/AgentGuidelines/Guidelines/GitHub/PullRequests.md index 5010a47..899666f 100644 --- a/AgentGuidelines/Guidelines/GitHub/PullRequests.md +++ b/AgentGuidelines/Guidelines/GitHub/PullRequests.md @@ -20,7 +20,10 @@ Opening a pull request starts review; it does not authorize merging it. 5. If feedback should not be implemented, reply in the original thread with a concise technical reason. 6. Reply to implemented feedback with what changed and where. 7. Resolve a thread only after its concern has been addressed or explicitly declined. -8. Recheck the pull request immediately before merge for late comments and check-state changes. +8. After addressing review comments, update the pull-request description so it matches the current implementation, validation, and any remaining limitations. +9. Recheck the pull request immediately before merge for late comments and check-state changes. + +When replying with a commit reference, write the commit hash as raw text without backticks (for example, the hash 185c04f should remain 185c04f). GitHub then auto-links the hash to the commit. A thumbs-up or clean Codex review satisfies the agent-review step, but it does not replace any human approval required by the repository. Do not enable auto-merge before all review gates are satisfied. diff --git a/AgentGuidelines/Guidelines/Logging.md b/AgentGuidelines/Guidelines/Logging.md new file mode 100644 index 0000000..498c356 --- /dev/null +++ b/AgentGuidelines/Guidelines/Logging.md @@ -0,0 +1,74 @@ +# Logging + +Use this guide for Apple-platform applications and Swift packages that emit runtime diagnostics. Logging should improve observability without changing behavior, exposing sensitive data, or overwhelming the console. + +## Ownership + +- Each application or package owns the logs for operations it implements. +- A consuming application logs its own orchestration and lifecycle events. It must not reproduce or reformat a dependency's internal steps or outcomes. +- A reusable package describes events using its own domain language. Do not introduce concepts from one current client into package categories or messages. +- Ownership does not require every API or package to emit logs. Pure utilities and operations without a meaningful diagnostic event may emit nothing. +- Logging is a side effect. It must not affect returned values, state transitions, error handling, or control flow. +- Architectures that isolate side effects must call a logging package from an allowed side-effect boundary, such as middleware or a service, rather than from a pure reducer. + +## AppLogger and identity + +- Use the shared [AppLogger Swift package](https://github.com/thatfactory/applogger) rather than `print`, direct `Logger` instances, or project-specific logging backends that duplicate it. +- Add the package's `AppLogger` library product to each target that emits logs. Follow the package's current integration instructions for dependency configuration and version requirements. +- Give each artifact an explicit, stable, lowercase subsystem in reverse-DNS form: `com.thatfactory.`. +- A package always uses its own subsystem, even when its code runs inside a consuming application. This allows filtering all ThatFactory logs or one artifact independently. +- Choose stable categories from the artifact's reusable domain. Categories are not a global vocabulary: a language-evaluation package might use `evaluation`, a progression engine might use `progression`, and an application might use `session` or `lifecycle`. +- Keep the category set as small and generic as possible while still distinguishing meaningful operations within that artifact. +- Do not add a category solely because one current client uses that concept. + +## Package emoji + +- Every log message emitted by a ThatFactory package starts with that package's canonical emoji followed by one space. +- Use the emoji registered in the [ThatFactory Swift Package Collection](https://github.com/thatfactory/swift-package-collection). +- Declare the selected emoji in the package's local instructions or documentation. +- Route package logging through one package-local gateway that owns the subsystem, categories, and emoji prefix. Production call sites must not construct unprefixed package messages directly. + +## Message design + +- Keep each message short, direct, and on one line. +- Prefer one completion or outcome message over separate start, intermediate, and completion messages. +- Use a compact action followed by stable `key=value` metadata when context is useful: + + ```text + evaluate | type=classification, correct=true, score=10 + ``` + +- Do not log routine property access, initializers, collection iterations, or other high-frequency implementation details. +- Use `.debug` for routine diagnostics, `.info` or `.default` for meaningful lifecycle events, `.error` for failures, and `.fault` only for severe conditions that indicate a system-level problem. +- Do not prepend the current time or date. Apple unified logging already records the event timestamp. +- Use AppLogger's `Date.formattedLogTimestamp()` and `TimeInterval.formattedLogDuration()` only when a domain date or elapsed duration is part of the event itself. + +## Privacy + +- Never log credentials, tokens, secrets, personal data, prompts, submitted answers, or other user-generated content as public metadata. +- Prefer omitting sensitive values. If a diagnostic genuinely requires them, mark the entire AppLogger message private. +- Do not emit complete models, collections, or application-state snapshots in routine logs. +- Any temporary state snapshot must be debug-only, explicitly enabled, and private. + +## Testing + +- Keep message rendering independently testable through an internal formatter, injectable sink, or similarly narrow seam. +- Verify that every package message starts with its canonical emoji. +- Verify the stable category, meaningful fields, privacy choice, log level, and single-emission behavior for each logged operation. +- Do not make tests depend on querying the operating system's persisted log store. + +## Filtering + +Use the subsystem in Console or the macOS `/usr/bin/log` command. For example: + +```sh +/usr/bin/log stream --level debug \ + --predicate 'subsystem BEGINSWITH "com.thatfactory"' +``` + +Filter one artifact with an exact subsystem: + +```sh +/usr/bin/log stream --level debug \ + --predicate 'subsystem == "com.thatfactory.example"' +``` diff --git a/AgentGuidelines/Guidelines/Packages.md b/AgentGuidelines/Guidelines/Packages.md index 8bb04fa..da332c3 100644 --- a/AgentGuidelines/Guidelines/Packages.md +++ b/AgentGuidelines/Guidelines/Packages.md @@ -39,9 +39,14 @@ The common package baseline is Swift, Xcode, Platforms, License, and CI. Add opt - Do not add application Redux, navigation, persistence, or product policy to a generic package. - Keep public APIs minimal and stable. Prefer composing focused types over introducing umbrella abstractions before multiple consumers need them. - Declare platform and Swift toolchain requirements explicitly in `Package.swift`. +- New Swift packages must start on the latest supported Swift language and toolchain version. Before adding a major package capability to an older package, plan and complete the required Swift/toolchain modernization first. - Put sources under `Sources//` and tests under `Tests/Tests/`. - Keep resources in the target that owns them and use the package bundle for lookup. +## Logging + +Packages own any diagnostics emitted by their implementation. Follow the shared [logging guide](Logging.md) for AppLogger usage, subsystem identity, package emoji prefixes, domain-owned categories, concise messages, privacy, and test coverage. A consuming application must not reproduce package-internal logs. + ## Development workflow 1. Read the package's local `AGENTS.md`, README, DocC, and public API before changing behavior. @@ -51,6 +56,22 @@ The common package baseline is Swift, Xcode, Platforms, License, and CI. Add opt 5. Integrate the package into a consumer locally only when consumer behavior must also be verified. 6. Avoid committing consumer-specific workarounds into the package when the behavior belongs in the consumer. +## DocC documentation + +DocC is the default documentation format for public Swift packages. Document public APIs with `///` DocC comments and keep package-level conceptual material in a DocC catalog when it needs more than declaration comments. + +Before adopting the DocC command, an existing package must be updated to the latest supported Swift toolchain and declare the Swift-DocC plugin dependency in `Package.swift` (for example, `.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "")`). New packages must declare this prerequisite from the beginning when they publish DocC. + +Packages that publish documentation must build and deploy their DocC site as part of the release workflow: + +1. Run tests before documentation generation. +2. Generate static-hosting documentation with `swift package generate-documentation --target --disable-indexing --output-path ./public --transform-for-static-hosting --hosting-base-path `. +3. Add a root redirect to `//documentation//`. +4. Upload `./public` with `actions/upload-pages-artifact` and deploy it with `actions/deploy-pages`. +5. Grant the workflow `pages: write` and `id-token: write` permissions and expose the deployed URL in the README through a DocC badge. + +The release job must publish documentation only after the release has been approved, merged, tagged, and published. Verify the generated site locally when practical and keep the README badge URL aligned with the repository's GitHub Pages site. + ## Local integration - Use Xcode's local-package workflow or an explicit temporary local dependency while developing package and consumer changes together. @@ -60,15 +81,18 @@ The common package baseline is Swift, Xcode, Platforms, License, and CI. Add opt ## Releases +Never release a package directly from unreviewed changes. Every release change must first be submitted through a pull request, reviewed, and approved. This rule applies to `agent-guidelines` itself as well as every consumer package. Create and publish the release only after the PR has merged. + For ThatFactory packages, โ€œrelease a new versionโ€ means: 1. Choose a semantic version appropriate to compatibility. 2. Update public documentation and release notes. 3. Run the declared CI/test workflow. -4. Commit the release state. -5. Create and push the matching Git tag. -6. Create a GitHub release for that tag. -7. Use real multiline release notes and backticks around technical names and versions. +4. Open a pull request containing the release state and wait for approval. +5. Merge the approved pull request. +6. Create and push the matching Git tag. +7. Create a GitHub release for that tag. +8. Use real multiline release notes and backticks around technical names and versions. When using a CLI, pass multiline notes through a file so GitHub renders line breaks correctly. diff --git a/AgentGuidelines/Guidelines/Testing/UnitTesting.md b/AgentGuidelines/Guidelines/Testing/UnitTesting.md index 4bae316..a694331 100644 --- a/AgentGuidelines/Guidelines/Testing/UnitTesting.md +++ b/AgentGuidelines/Guidelines/Testing/UnitTesting.md @@ -15,6 +15,8 @@ - Keep each test focused on one behavior and name it in domain language. - Mirror production physical folders under the test target. - Put reusable mocks and fixtures under the test target's `Mocks/` folder. +- Use shared test tags for recurring classification and keep their declarations alphabetical. Prefer small composable tags that can be combined to describe a test without repeating ad hoc metadata. +- Add a short `///` comment describing each mock's test purpose. ## Assertions and control flow diff --git a/AgentGuidelines/README.md b/AgentGuidelines/README.md index 4375441..9f5c94c 100644 --- a/AgentGuidelines/README.md +++ b/AgentGuidelines/README.md @@ -9,7 +9,7 @@ # Agent Guidelines -`agent-guidelines` is ThatFactory's public, versioned source of truth for reusable instructions given to coding agents. It centralizes stable decisions about Swift development, Redux architecture, testing, documentation, packages, CI/CD, localization, and Xcode tooling while leaving product context and exceptions in each consuming repository. +`agent-guidelines` is ThatFactory's public, versioned source of truth for reusable instructions given to coding agents. It centralizes stable decisions about Swift development, Redux architecture, testing, documentation, logging, packages, CI/CD, localization, and Xcode tooling while leaving product context and exceptions in each consuming repository. The repository contains documentation, not a Swift product. Consumers install a tagged release as a Git subtree at `AgentGuidelines/`, so every agent sees ordinary version-controlled files at predictable paths. @@ -66,14 +66,16 @@ The subtree does not automatically import every guide into an agent's context. A - [Localization](Guidelines/Swift/Localization.md) - [Unit and integration testing](Guidelines/Testing/UnitTesting.md) - [Documentation](Guidelines/Documentation.md) +- [Logging](Guidelines/Logging.md) - [Swift packages](Guidelines/Packages.md) +- [Development and reusability](Guidelines/Development.md) - [CI/CD](Guidelines/CICD.md) - [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) - [GitHub pull requests](Guidelines/GitHub/PullRequests.md) - [Xcode MCP and visual verification](Guidelines/Xcode/MCP.md) - [Xcode security audits](Guidelines/Xcode/Security.md) -Only reference the guides that apply. A UI-agnostic package normally uses Swift, style, testing, documentation, packages, CI/CD, and Xcode guidance, but not Redux or SwiftUI guidance. +Only reference the guides that apply. A UI-agnostic package normally uses Swift, style, testing, documentation, logging, packages, CI/CD, and Xcode guidance, but not Redux or SwiftUI guidance. ## Add to a consumer @@ -83,7 +85,7 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.3 \ + 0.0.7 \ --squash ``` @@ -97,7 +99,7 @@ Review the target release's changelog, then pull it deliberately: git subtree pull \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - \ + 0.0.7 \ --squash ``` @@ -115,7 +117,8 @@ Confirm `AgentGuidelines/VERSION`, review the subtree diff, validate local `AGEN 3. Bring over durable policy, not the exported skill text or an SDK API catalog. 4. Remove obsolete or conflicting rules instead of accumulating historical alternatives. 5. Run `python3 Scripts/validate_guidelines.py`. -6. Update `VERSION` and `CHANGELOG.md`, then create the matching tag and GitHub release. +6. Update `VERSION` and `CHANGELOG.md`, open a pull request, and wait for approval before merging. +7. After the pull request has merged, create the matching tag and GitHub release. ## Precedence diff --git a/AgentGuidelines/Templates/AGENTS.md b/AgentGuidelines/Templates/AGENTS.md index b28c118..66d89dd 100644 --- a/AgentGuidelines/Templates/AGENTS.md +++ b/AgentGuidelines/Templates/AGENTS.md @@ -15,6 +15,7 @@ Read only the guides relevant to the task: - [Localization](AgentGuidelines/Guidelines/Swift/Localization.md) - [Unit and integration testing](AgentGuidelines/Guidelines/Testing/UnitTesting.md) - [Documentation](AgentGuidelines/Guidelines/Documentation.md) +- [Logging](AgentGuidelines/Guidelines/Logging.md) - [Packages](AgentGuidelines/Guidelines/Packages.md) - [CI/CD](AgentGuidelines/Guidelines/CICD.md) - [Git repositories and SSH-first cloning](AgentGuidelines/Guidelines/Git/Repositories.md) diff --git a/AgentGuidelines/VERSION b/AgentGuidelines/VERSION index bcab45a..5a5831a 100644 --- a/AgentGuidelines/VERSION +++ b/AgentGuidelines/VERSION @@ -1 +1 @@ -0.0.3 +0.0.7 diff --git a/Package.resolved b/Package.resolved index f6c0677..06507de 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "a7cf709b54776e438f795355cd120c1195b4865978d0fb04447d8782f185c8a3", + "originHash" : "537618524d8febb222b9556d3d27add53e153103a47050a496e01cc2e96f821f", "pins" : [ + { + "identity" : "applogger", + "kind" : "remoteSourceControl", + "location" : "https://github.com/thatfactory/applogger", + "state" : { + "revision" : "75dbe6e22170cb7e7507645cc23ab31611a07c84", + "version" : "1.1.0" + } + }, { "identity" : "swift-docc-plugin", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index db38baf..20faee8 100644 --- a/Package.swift +++ b/Package.swift @@ -17,15 +17,28 @@ let package = Package( ) ], dependencies: [ + .package(url: "https://github.com/thatfactory/applogger", from: "1.1.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0") ], targets: [ .target( - name: "ProgressionKit" + name: "ProgressionKit", + dependencies: [ + .product( + name: "AppLogger", + package: "applogger" + ) + ] ), .testTarget( name: "ProgressionKitTests", - dependencies: ["ProgressionKit"] + dependencies: [ + "ProgressionKit", + .product( + name: "AppLogger", + package: "applogger" + ) + ] ) ] ) diff --git a/README.md b/README.md index fdb244f..10f4e18 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ It models: The package is deliberately content-agnostic. Host apps decide what a track, content item, and tier mean, then feed those identifiers into `ProgressionKit`. +## Logging + +ProgressionKit logs concise progression outcomes through [AppLogger](https://github.com/thatfactory/applogger) with subsystem `com.thatfactory.progressionkit` and category `progression`. + +Every package-owned line starts with `๐Ÿ“ˆ` and includes only the XP granted, resulting player level, and number of newly unlocked tiers. ProgressionKit does not log content, track, or tier identifiers. + ## Implemented APIs - `PKEngine`: applies a progression event to a profile and returns the updated profile plus derived progress values. @@ -218,7 +224,7 @@ In your `Package.swift`, add `ProgressionKit` as a dependency: dependencies: [ .package( url: "https://github.com/thatfactory/progressionkit", - from: "0.1.3" + from: "0.1.4" ) ] ``` diff --git a/Sources/ProgressionKit/PKEngine.swift b/Sources/ProgressionKit/PKEngine.swift index a0b241e..0873954 100644 --- a/Sources/ProgressionKit/PKEngine.swift +++ b/Sources/ProgressionKit/PKEngine.swift @@ -1,6 +1,8 @@ import Foundation /// Applies progression events to profiles using a deterministic rule set. +/// +/// Each application emits one `๐Ÿ“ˆ` debug log containing only the XP granted, resulting level, and unlock count. public enum PKEngine { /// Applies one progression event to a player profile. /// @@ -103,7 +105,7 @@ private extension PKEngine { let playerLevel = (profile.totalXP / config.levelXP) + 1 let xpIntoLevel = profile.totalXP % config.levelXP - return PKUpdate( + let update = PKUpdate( didGrantXP: didGrantXP, newlyUnlockedTierIDs: newlyUnlockedTierIDs, playerLevel: playerLevel, @@ -111,5 +113,10 @@ private extension PKEngine { xpForNextLevel: config.levelXP, xpIntoLevel: xpIntoLevel ) + PKLogging.logProgression( + update: update, + xpGranted: didGrantXP ? config.masteryXP : 0 + ) + return update } } diff --git a/Sources/ProgressionKit/PKLogging.swift b/Sources/ProgressionKit/PKLogging.swift new file mode 100644 index 0000000..5357884 --- /dev/null +++ b/Sources/ProgressionKit/PKLogging.swift @@ -0,0 +1,42 @@ +import AppLogger + +/// Routes ProgressionKit-owned diagnostics through the package logging identity. +enum PKLogging { + typealias Sink = @Sendable (AppLogLevel, PKLogCategory, String, Bool) -> Void + + static let emoji = "๐Ÿ“ˆ" + static let subsystem = "com.thatfactory.progressionkit" + + @TaskLocal + static var sink: Sink = { level, category, message, isPrivate in + let logger = AppLogger( + subsystem: subsystem, + category: category.rawValue + ) + logger.log( + level: level, + message, + isPrivate: isPrivate + ) + } + + static func logProgression( + update: PKUpdate, + xpGranted: Int + ) { + sink( + .debug, + .progression, + """ + \(emoji) apply | xpGranted=\(xpGranted), \ + level=\(update.playerLevel), unlocked=\(update.newlyUnlockedTierIDs.count) + """, + false + ) + } +} + +/// Identifies stable diagnostic categories owned by ProgressionKit. +enum PKLogCategory: String, Sendable { + case progression +} diff --git a/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md b/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md index f8f659b..0754f83 100644 --- a/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md +++ b/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md @@ -18,6 +18,8 @@ It models XP gain from successful performance, player levels derived from total The package is content-agnostic. Host apps decide what a track, content item, and tier mean, then feed those identifiers into ``PKEngine``. +Each applied event emits one concise `๐Ÿ“ˆ` debug log through `AppLogger`. ProgressionKit uses subsystem `com.thatfactory.progressionkit`, category `progression`, and omits content, track, and tier identifiers. + ## Usage ```swift diff --git a/Tests/ProgressionKitTests/PKLoggingTests.swift b/Tests/ProgressionKitTests/PKLoggingTests.swift new file mode 100644 index 0000000..ef2945f --- /dev/null +++ b/Tests/ProgressionKitTests/PKLoggingTests.swift @@ -0,0 +1,100 @@ +import AppLogger +import Foundation +import Testing +@testable import ProgressionKit + +@Suite struct PKLoggingTests { + @Test func applyingEventLogsOnePackageOwnedOutcome() throws { + // Given + let recorder = PKLogRecorder() + let event = PKEvent( + contentID: "Sensitive content", + trackID: "Sensitive track", + tierID: "beginner", + wasSuccessful: true + ) + let config = PKConfig( + levelXP: 10, + masteryXP: 15, + tierOrder: ["beginner", "intermediate"], + masteryRequirement: 1 + ) + + // When + PKLogging.$sink.withValue(recorder.record) { + _ = PKEngine.apply( + event: event, + to: PKProfile(), + config: config + ) + } + + // Then + let entry = try #require(recorder.entries.first) + #expect(recorder.entries.count == 1) + #expect(entry.category == .progression) + #expect(!entry.isPrivate) + #expect(entry.message == "๐Ÿ“ˆ apply | xpGranted=15, level=2, unlocked=1") + #expect(!entry.message.contains(event.contentID)) + #expect(!entry.message.contains(event.trackID)) + #expect(!entry.message.contains(event.tierID)) + #expect(PKLogging.subsystem == "com.thatfactory.progressionkit") + expectDebug(entry.level) + } +} + +// MARK: - Private + +private func expectDebug( + _ level: AppLogLevel, + sourceLocation: SourceLocation = #_sourceLocation +) { + guard case .debug = level else { + Issue.record( + "Expected a debug log level.", + sourceLocation: sourceLocation + ) + return + } +} + +/// Records ProgressionKit log entries emitted during a test. +private final class PKLogRecorder: @unchecked Sendable { + private var internalEntries: [PKRecordedLog] = [] + private let lock = NSLock() + + var entries: [PKRecordedLog] { + lock.lock() + defer { + lock.unlock() + } + return internalEntries + } + + func record( + _ level: AppLogLevel, + _ category: PKLogCategory, + _ message: String, + _ isPrivate: Bool + ) { + lock.lock() + defer { + lock.unlock() + } + internalEntries.append( + PKRecordedLog( + level: level, + category: category, + message: message, + isPrivate: isPrivate + ) + ) + } +} + +private struct PKRecordedLog { + let level: AppLogLevel + let category: PKLogCategory + let message: String + let isPrivate: Bool +}