From 83d3c0f69d8783b9b5ccbb98a37c29653d86052f Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Thu, 23 Jul 2026 17:12:11 +0200 Subject: [PATCH 1/2] Squashed 'AgentGuidelines/' changes from 2958ce1..c037349 c037349 Define shared logging ownership and conventions (#8) 520eb5b Promote reusable project guidance to generic agent-guidelines (#7) f175353 Document DocC package publishing (#6) e439e1a Require reviewed PRs before releases (#5) git-subtree-dir: AgentGuidelines git-subtree-split: c0373496eb1a06b0a6ba335e5141be71b74cf7c6 --- .github/workflows/ci.yml | 32 +- .github/workflows/nightly.yml | 31 -- .github/workflows/release.yml | 125 ++----- .gitignore | 12 +- AGENTS.md | 73 ++-- AgentGuidelines/.github/workflows/ci.yml | 23 -- AgentGuidelines/.github/workflows/release.yml | 44 --- AgentGuidelines/.gitignore | 3 - AgentGuidelines/AGENTS.md | 38 -- AgentGuidelines/Guidelines/CICD.md | 42 --- .../Guidelines/Git/Repositories.md | 22 -- AgentGuidelines/LICENSE | 22 -- AgentGuidelines/README.md | 129 ------- AgentGuidelines/VERSION | 1 - AgentGuidelines/CHANGELOG.md => CHANGELOG.md | 31 ++ .../Architecture/Redux.md | 30 +- Guidelines/CICD.md | 63 ++++ Guidelines/Development.md | 13 + .../Documentation.md | 4 + Guidelines/Git/Repositories.md | 42 +++ .../GitHub/PullRequests.md | 5 +- Guidelines/Logging.md | 74 ++++ .../Guidelines => Guidelines}/Packages.md | 32 +- .../Swift/Localization.md | 0 .../Guidelines => Guidelines}/Swift/Swift.md | 0 .../Swift/SwiftLint.md | 0 .../Swift/SwiftStyle.md | 0 .../Swift/SwiftUI.md | 0 .../Testing/UnitTesting.md | 2 + .../Guidelines => Guidelines}/Xcode/MCP.md | 0 .../Xcode/Security.md | 0 LICENSE | 1 + Package.resolved | 24 -- Package.swift | 31 -- README.md | 332 ++++++------------ .../validate_guidelines.py | 0 Sources/ProgressionKit/PKConfig.swift | 47 --- Sources/ProgressionKit/PKEngine.swift | 115 ------ Sources/ProgressionKit/PKEvent.swift | 35 -- Sources/ProgressionKit/PKProfile.swift | 23 -- Sources/ProgressionKit/PKTierProgress.swift | 14 - Sources/ProgressionKit/PKTrackProgress.swift | 23 -- Sources/ProgressionKit/PKUpdate.swift | 47 --- .../ProgressionKit.docc/ProgressionKit.md | 67 ---- Sources/ProgressionKit/ProgressionKit.swift | 1 - .../Templates => Templates}/AGENTS.md | 1 + .../ProgressionKitTests.swift | 196 ----------- .../test_validate_guidelines.py | 0 VERSION | 1 + 49 files changed, 490 insertions(+), 1361 deletions(-) delete mode 100644 .github/workflows/nightly.yml delete mode 100644 AgentGuidelines/.github/workflows/ci.yml delete mode 100644 AgentGuidelines/.github/workflows/release.yml delete mode 100644 AgentGuidelines/.gitignore delete mode 100644 AgentGuidelines/AGENTS.md delete mode 100644 AgentGuidelines/Guidelines/CICD.md delete mode 100644 AgentGuidelines/Guidelines/Git/Repositories.md delete mode 100644 AgentGuidelines/LICENSE delete mode 100644 AgentGuidelines/README.md delete mode 100644 AgentGuidelines/VERSION rename AgentGuidelines/CHANGELOG.md => CHANGELOG.md (62%) rename {AgentGuidelines/Guidelines => Guidelines}/Architecture/Redux.md (86%) create mode 100644 Guidelines/CICD.md create mode 100644 Guidelines/Development.md rename {AgentGuidelines/Guidelines => Guidelines}/Documentation.md (90%) create mode 100644 Guidelines/Git/Repositories.md rename {AgentGuidelines/Guidelines => Guidelines}/GitHub/PullRequests.md (92%) create mode 100644 Guidelines/Logging.md rename {AgentGuidelines/Guidelines => Guidelines}/Packages.md (59%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/Localization.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/Swift.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/SwiftLint.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/SwiftStyle.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Swift/SwiftUI.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Testing/UnitTesting.md (91%) rename {AgentGuidelines/Guidelines => Guidelines}/Xcode/MCP.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Xcode/Security.md (100%) delete mode 100644 Package.resolved delete mode 100644 Package.swift rename {AgentGuidelines/Scripts => Scripts}/validate_guidelines.py (100%) delete mode 100644 Sources/ProgressionKit/PKConfig.swift delete mode 100644 Sources/ProgressionKit/PKEngine.swift delete mode 100644 Sources/ProgressionKit/PKEvent.swift delete mode 100644 Sources/ProgressionKit/PKProfile.swift delete mode 100644 Sources/ProgressionKit/PKTierProgress.swift delete mode 100644 Sources/ProgressionKit/PKTrackProgress.swift delete mode 100644 Sources/ProgressionKit/PKUpdate.swift delete mode 100644 Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md delete mode 100644 Sources/ProgressionKit/ProgressionKit.swift rename {AgentGuidelines/Templates => Templates}/AGENTS.md (97%) delete mode 100644 Tests/ProgressionKitTests/ProgressionKitTests.swift rename {AgentGuidelines/Tests => Tests}/test_validate_guidelines.py (100%) create mode 100644 VERSION diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd6d684..5831117 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,23 @@ ---- name: CI on: + pull_request: push: branches: - main - pull_request: -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true +permissions: + contents: read jobs: - test: - name: Test - runs-on: [self-hosted, macOS] + validate: + name: Validate guidelines + runs-on: ubuntu-latest steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true + - name: Checkout + uses: actions/checkout@v7 - - name: Clear SwiftPM Caches + - name: Validate run: | - rm -rf ~/.swiftpm - rm -rf ~/Library/Caches/org.swift.swiftpm - rm -rf ~/Library/org.swift.swiftpm - rm -rf .swiftpm - rm -rf .build - - - name: Run Tests - run: swift test -v + python3 -m unittest discover -s Tests + python3 Scripts/validate_guidelines.py diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index 5b67da9..0000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Nightly Tests - -on: - schedule: - - cron: '0 4 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - nightly_tests: - name: Nightly Tests - runs-on: [self-hosted, macOS] - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true - - - name: Clear SwiftPM Caches - run: | - rm -rf ~/.swiftpm - rm -rf ~/Library/Caches/org.swift.swiftpm - rm -rf ~/Library/org.swift.swiftpm - rm -rf .swiftpm - rm -rf .build - - - name: Run Tests - run: swift test -v diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c84201..20b4c8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,109 +1,44 @@ ---- name: Release -run-name: Release ${{ github.event.release.tag_name }} on: - release: - types: - - published + push: + tags: + - "*.*.*" permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + contents: write jobs: - test: - name: Test Release - runs-on: [self-hosted, macOS] + release: + name: Create GitHub release + runs-on: ubuntu-latest steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true + - name: Checkout + uses: actions/checkout@v7 - - name: Clear SwiftPM Caches - run: | - rm -rf ~/.swiftpm - rm -rf ~/Library/Caches/org.swift.swiftpm - rm -rf ~/Library/org.swift.swiftpm - rm -rf .swiftpm - rm -rf .build + - name: Validate guidelines + run: python3 Scripts/validate_guidelines.py - - name: Run Tests - run: swift test -v - - build_docs: - name: Build DocC - runs-on: [self-hosted, macOS] - needs: test - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - clean: true - - - name: Generate DocC + - name: Validate tag run: | - set -euo pipefail - swift package --allow-writing-to-directory ./public generate-documentation \ - --target ProgressionKit \ - --disable-indexing \ - --output-path ./public \ - --transform-for-static-hosting \ - --hosting-base-path progressionkit - - cat > ./public/index.html <<'INDEX' - - - ProgressionKit Documentation - INDEX + version="$(tr -d '[:space:]' < VERSION)" + test "$GITHUB_REF_NAME" = "$version" - - name: Upload Pages Artifact - uses: actions/upload-pages-artifact@v5 - with: - path: ./public - name: github-pages - - deploy_docs: - name: Deploy DocC - needs: build_docs - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 - - notify_package_collection: - name: Notify Package Collection - runs-on: ubuntu-latest - needs: deploy_docs - steps: - - name: Trigger Swift Package Collection Rebuild + - name: Prepare release notes + run: | + version="$(tr -d '[:space:]' < VERSION)" + awk -v version="$version" ' + index($0, "## [" version "]") == 1 { capture = 1; next } + capture && /^## \[/ { exit } + capture { print } + ' CHANGELOG.md > release-notes.md + test -s release-notes.md + + - name: Create release env: - COLLECTION_REPO: thatfactory/swift-package-collection - WORKFLOW_FILE: publish.yml - REF: main - GH_TOKEN: ${{ secrets.COLLECTION_TRIGGER_TOKEN }} - SOURCE_REPO: ${{ github.repository }} - SOURCE_VERSION: ${{ github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} run: | - set -euo pipefail - - if [ -z "${GH_TOKEN:-}" ]; then - echo "Missing COLLECTION_TRIGGER_TOKEN secret" - exit 1 - fi - - curl -sS -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GH_TOKEN" \ - "https://api.github.com/repos/$COLLECTION_REPO/actions/workflows/$WORKFLOW_FILE/dispatches" \ - -d "{\"ref\":\"$REF\",\"inputs\":{\"source_repo\":\"$SOURCE_REPO\",\"source_version\":\"$SOURCE_VERSION\"}}" + gh release create "$GITHUB_REF_NAME" \ + --verify-tag \ + --title "$GITHUB_REF_NAME" \ + --notes-file release-notes.md diff --git a/.gitignore b/.gitignore index 08c2ed7..dff2f41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,3 @@ .DS_Store -/.build -/Packages -/*.xcodeproj -*.xcworkspace -xcuserdata/ -Package.resolved -DerivedData/ -.swiftpm/configuration/registries.json -.netrc -/public-check/ +__pycache__/ +*.py[cod] diff --git a/AGENTS.md b/AGENTS.md index 5cc7679..f3cc3d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,41 +1,54 @@ -# ProgressionKit +# Agent Guidelines -## Context +## Purpose -ProgressionKit is a pure Swift package for deterministic XP, player levels, track mastery, and tier unlocks. Read [README.md](README.md) and the DocC catalog before changing public behavior. +This public repository is the versioned source of truth for reusable ThatFactory agent guidance. Keep it generic enough to apply to multiple applications and Swift packages. Product decisions, concrete project paths, and exceptions belong in each consumer repository. -The package is content-, storage-, UI-, and application-architecture agnostic. Host applications decide what content, tracks, tiers, persistence, and presentation mean. +## Sources of truth -## Shared guidelines +- Use official Apple documentation for Apple APIs and Xcode behavior. +- 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. -Read only the guides relevant to the task: +## Documentation changes -- [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) -- [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) -- [SwiftLint](AgentGuidelines/Guidelines/Swift/SwiftLint.md) -- [Unit and integration testing](AgentGuidelines/Guidelines/Testing/UnitTesting.md) -- [Documentation](AgentGuidelines/Guidelines/Documentation.md) -- [Packages](AgentGuidelines/Guidelines/Packages.md) -- [CI/CD](AgentGuidelines/Guidelines/CICD.md) -- [Git repositories and SSH-first cloning](AgentGuidelines/Guidelines/Git/Repositories.md) -- [GitHub pull requests](AgentGuidelines/Guidelines/GitHub/PullRequests.md) -- [Xcode MCP](AgentGuidelines/Guidelines/Xcode/MCP.md) -- [Xcode security audits](AgentGuidelines/Guidelines/Xcode/Security.md) +- Keep each rule in the narrowest relevant guide and link to it rather than duplicating it. +- Use physical folder terminology for Xcode projects. Do not call filesystem folders Xcode groups. +- Keep examples generic and concise. +- 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: -Redux, SwiftUI, and application-localization guidance do not apply to the package target. + ```sh + git subtree add \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + \ + --squash -## Physical folder map + git subtree pull \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + \ + --squash + ``` -| Role | Physical folder | -|---|---| -| Package sources | `Sources/ProgressionKit/` | -| DocC catalog | `Sources/ProgressionKit/ProgressionKit.docc/` | -| Unit tests | `Tests/ProgressionKitTests/` | +## Validation -## Package specialization +Run: -- Keep progression updates deterministic for the same profile, event, and configuration. -- Do not add storage, network, UI, Redux, or game-content dependencies. -- 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. +```sh +python3 Scripts/validate_guidelines.py +``` + +Fix every validation failure before releasing a version. + +## Releases + +- Use semantic versioning. +- Create a Git tag and GitHub release matching `VERSION`. +- Consumer repositories adopt releases deliberately through Git subtree updates. +- Follow [the pull-request review workflow](Guidelines/GitHub/PullRequests.md) before merging any release change. diff --git a/AgentGuidelines/.github/workflows/ci.yml b/AgentGuidelines/.github/workflows/ci.yml deleted file mode 100644 index 5831117..0000000 --- a/AgentGuidelines/.github/workflows/ci.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -jobs: - validate: - name: Validate guidelines - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Validate - run: | - python3 -m unittest discover -s Tests - python3 Scripts/validate_guidelines.py diff --git a/AgentGuidelines/.github/workflows/release.yml b/AgentGuidelines/.github/workflows/release.yml deleted file mode 100644 index 20b4c8f..0000000 --- a/AgentGuidelines/.github/workflows/release.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Release - -on: - push: - tags: - - "*.*.*" - -permissions: - contents: write - -jobs: - release: - name: Create GitHub release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Validate guidelines - run: python3 Scripts/validate_guidelines.py - - - name: Validate tag - run: | - version="$(tr -d '[:space:]' < VERSION)" - test "$GITHUB_REF_NAME" = "$version" - - - name: Prepare release notes - run: | - version="$(tr -d '[:space:]' < VERSION)" - awk -v version="$version" ' - index($0, "## [" version "]") == 1 { capture = 1; next } - capture && /^## \[/ { exit } - capture { print } - ' CHANGELOG.md > release-notes.md - test -s release-notes.md - - - name: Create release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release create "$GITHUB_REF_NAME" \ - --verify-tag \ - --title "$GITHUB_REF_NAME" \ - --notes-file release-notes.md diff --git a/AgentGuidelines/.gitignore b/AgentGuidelines/.gitignore deleted file mode 100644 index dff2f41..0000000 --- a/AgentGuidelines/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -__pycache__/ -*.py[cod] diff --git a/AgentGuidelines/AGENTS.md b/AgentGuidelines/AGENTS.md deleted file mode 100644 index aef307d..0000000 --- a/AgentGuidelines/AGENTS.md +++ /dev/null @@ -1,38 +0,0 @@ -# Agent Guidelines - -## Purpose - -This public repository is the versioned source of truth for reusable ThatFactory agent guidance. Keep it generic enough to apply to multiple applications and Swift packages. Product decisions, concrete project paths, and exceptions belong in each consumer repository. - -## Sources of truth - -- Use official Apple documentation for Apple APIs and Xcode behavior. -- 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. - -## Documentation changes - -- Keep each rule in the narrowest relevant guide and link to it rather than duplicating it. -- Use physical folder terminology for Xcode projects. Do not call filesystem folders Xcode groups. -- Keep examples generic and concise. -- 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. - -## Validation - -Run: - -```sh -python3 Scripts/validate_guidelines.py -``` - -Fix every validation failure before releasing a version. - -## Releases - -- Use semantic versioning. -- Create a Git tag and GitHub release matching `VERSION`. -- Consumer repositories adopt releases deliberately through Git subtree updates. -- Follow [the pull-request review workflow](Guidelines/GitHub/PullRequests.md) before merging any release change. diff --git a/AgentGuidelines/Guidelines/CICD.md b/AgentGuidelines/Guidelines/CICD.md deleted file mode 100644 index a66886e..0000000 --- a/AgentGuidelines/Guidelines/CICD.md +++ /dev/null @@ -1,42 +0,0 @@ -# CI/CD - -## Workflow principles - -- Keep CI deterministic, reproducible, and aligned with the repository's supported Xcode, Swift, and platform versions. -- Treat warnings introduced by a change as failures even when the compiler does not. -- Prefer the smallest permissions required by each workflow and job. -- For a new workflow, use the latest stable major version of every GitHub Action available at the time of creation. -- Do not copy an older major version into a fresh workflow unless a documented compatibility constraint requires it. -- For existing workflows, review action release notes and update deliberately rather than allowing runtime deprecation warnings to accumulate. -- Pin third-party actions to an intentional version and review updates. -- 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 - -A typical Swift package validates: - -- package resolution; -- build; -- Swift Testing tests; -- DocC generation when the package publishes documentation; -- repository-specific lint or validation scripts. - -An Xcode application validates its declared scheme and test plan. Use the same project/workspace, configuration, and platform assumptions documented for local development. - -## Investigation - -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. - -## Releases - -- A release tag and GitHub release must match the intended semantic version. -- Release notes summarize user- or integrator-relevant changes since the previous release. -- Use a notes file for multiline CLI release descriptions. -- Do not publish a release from an unverified or dirty worktree. -- Follow the consumer's local instructions for deployment, signing, notarization, App Store, or documentation publishing steps. diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/AgentGuidelines/Guidelines/Git/Repositories.md deleted file mode 100644 index a85949a..0000000 --- a/AgentGuidelines/Guidelines/Git/Repositories.md +++ /dev/null @@ -1,22 +0,0 @@ -# Git Repositories - -Use these rules when cloning repositories or configuring remotes. - -## SSH-first cloning - -Clone a repository over SSH when the working copy may be used to commit, push, or open a pull request: - -```sh -git clone git@github.com:/.git -``` - -- Prefer an SSH `origin` so command-line tools and Git clients such as Fork can reuse the machine's GitHub SSH authentication. -- Do not create a push-capable working copy with an HTTPS `origin` unless the user or environment explicitly requires HTTPS. -- After cloning for development, use `git remote -v` to confirm that fetch and push URLs are correct. -- If an existing development clone has an HTTPS `origin`, change it only when requested or when the task explicitly includes remote setup: - - ```sh - git remote set-url origin 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. diff --git a/AgentGuidelines/LICENSE b/AgentGuidelines/LICENSE deleted file mode 100644 index 42d8021..0000000 --- a/AgentGuidelines/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2026 ThatFactory - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/AgentGuidelines/README.md b/AgentGuidelines/README.md deleted file mode 100644 index 4375441..0000000 --- a/AgentGuidelines/README.md +++ /dev/null @@ -1,129 +0,0 @@ -

- Xcode - Codex - Updated - Revision - License - CI -

- -# 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. - -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. - -## How it fits together - -```text - thatfactory/agent-guidelines - versioned GitHub repository - | - tagged release - e.g. 0.0.3 - | - git subtree add/pull - | - v -+---------------- Consumer project or package ----------------+ -| | -| AGENTS.md | -| |-- local product/package context | -| |-- concrete project paths | -| |-- local exceptions | -| `-- pointers to shared guidelines -----------------+ | -| | | -| AgentGuidelines/ | | -| |-- VERSION | | -| `-- Guidelines/ <----------------------------------+ | -| |-- Architecture/Redux.md | -| |-- Swift/SwiftUI.md | -| |-- Testing/UnitTesting.md | -| `-- Xcode/MCP.md | -| | -| Sources and project files | -+----------------------------+---------------------------------+ - | - reads instructions and project files - +----------+----------+ - v v - Codex Xcode agent - | - | Xcode MCP (`xcrun mcpbridge`) - v - Xcode -``` - -The subtree does not automatically import every guide into an agent's context. A consumer's root or folder-scoped `AGENTS.md` tells the agent which shared guides to read for the task. The nearest local `AGENTS.md` can specialize or override the shared baseline. - -## Guideline catalog - -- [Redux architecture and physical folder organization](Guidelines/Architecture/Redux.md) -- [Swift](Guidelines/Swift/Swift.md) -- [Swift style](Guidelines/Swift/SwiftStyle.md) -- [SwiftUI](Guidelines/Swift/SwiftUI.md) -- [SwiftLint](Guidelines/Swift/SwiftLint.md) -- [Localization](Guidelines/Swift/Localization.md) -- [Unit and integration testing](Guidelines/Testing/UnitTesting.md) -- [Documentation](Guidelines/Documentation.md) -- [Swift packages](Guidelines/Packages.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. - -## Add to a consumer - -From the consumer repository root, install a tagged release: - -```sh -git subtree add \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - 0.0.3 \ - --squash -``` - -Copy and adapt [the consumer template](Templates/AGENTS.md). Keep the consumer file small: describe the product or package, map its concrete physical folders, point to the applicable shared guides, and state only genuine exceptions. - -## Update a consumer - -Review the target release's changelog, then pull it deliberately: - -```sh -git subtree pull \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - \ - --squash -``` - -Confirm `AgentGuidelines/VERSION`, review the subtree diff, validate local `AGENTS.md` pointers, and run the consumer's relevant tests. Updates are intentionally not automatic: one guideline release cannot silently change every project. - -## Maintain the source of truth - -1. Export current Xcode skills to a temporary review location when a new Xcode release materially changes agent behavior: - - ```sh - xcrun agent skills export --output-dir - ``` - -2. Compare relevant guidance with this repository and official Apple documentation. -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. - -## Precedence - -For a consumer task, apply instructions in this order: - -1. The user's explicit request. -2. The nearest applicable consumer `AGENTS.md`. -3. The consumer root `AGENTS.md`. -4. The shared guides explicitly referenced by those files. - -Official Apple documentation remains authoritative for API behavior. A local convention can deliberately narrow a choice, but it must not rely on behavior contradicted by the current SDK documentation. diff --git a/AgentGuidelines/VERSION b/AgentGuidelines/VERSION deleted file mode 100644 index bcab45a..0000000 --- a/AgentGuidelines/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.3 diff --git a/AgentGuidelines/CHANGELOG.md b/CHANGELOG.md similarity index 62% rename from AgentGuidelines/CHANGELOG.md rename to CHANGELOG.md index 75a67e4..faa1a6b 100644 --- a/AgentGuidelines/CHANGELOG.md +++ b/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/Guidelines/Architecture/Redux.md similarity index 86% rename from AgentGuidelines/Guidelines/Architecture/Redux.md rename to Guidelines/Architecture/Redux.md index 77b20f6..56ab6f3 100644 --- a/AgentGuidelines/Guidelines/Architecture/Redux.md +++ b/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/Guidelines/CICD.md b/Guidelines/CICD.md new file mode 100644 index 0000000..c420331 --- /dev/null +++ b/Guidelines/CICD.md @@ -0,0 +1,63 @@ +# CI/CD + +## Workflow principles + +- Keep CI deterministic, reproducible, and aligned with the repository's supported Xcode, Swift, and platform versions. +- Treat warnings introduced by a change as failures even when the compiler does not. +- Prefer the smallest permissions required by each workflow and job. +- For a new workflow, use the latest stable major version of every GitHub Action available at the time of creation. +- Do not copy an older major version into a fresh workflow unless a documented compatibility constraint requires it. +- For existing workflows, review action release notes and update deliberately rather than allowing runtime deprecation warnings to accumulate. +- Pin third-party actions to an intentional version and review updates. +- 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. + +## `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: + +- package resolution; +- build; +- Swift Testing tests; +- DocC generation when the package publishes documentation; +- repository-specific lint or validation scripts. + +An Xcode application validates its declared scheme and test plan. Use the same project/workspace, configuration, and platform assumptions documented for local development. + +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 +``` + +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 + +- A release tag and GitHub release must match the intended semantic version. +- Release notes summarize user- or integrator-relevant changes since the previous release. +- Use a notes file for multiline CLI release descriptions. +- Do not publish a release from an unverified or dirty worktree. +- Follow the consumer's local instructions for deployment, signing, notarization, App Store, or documentation publishing steps. diff --git a/Guidelines/Development.md b/Guidelines/Development.md new file mode 100644 index 0000000..8ab764d --- /dev/null +++ b/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/Guidelines/Documentation.md similarity index 90% rename from AgentGuidelines/Guidelines/Documentation.md rename to Guidelines/Documentation.md index b988361..2a0d327 100644 --- a/AgentGuidelines/Guidelines/Documentation.md +++ b/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/Guidelines/Git/Repositories.md b/Guidelines/Git/Repositories.md new file mode 100644 index 0000000..bff7933 --- /dev/null +++ b/Guidelines/Git/Repositories.md @@ -0,0 +1,42 @@ +# Git Repositories + +Use these rules when cloning repositories or configuring remotes. + +## SSH-first cloning + +Clone a repository over SSH when the working copy may be used to commit, push, or open a pull request: + +```sh +git clone git@github.com:/.git +``` + +- Prefer an SSH `origin` so command-line tools and Git clients such as Fork can reuse the machine's GitHub SSH authentication. +- Do not create a push-capable working copy with an HTTPS `origin` unless the user or environment explicitly requires HTTPS. +- After cloning for development, use `git remote -v` to confirm that fetch and push URLs are correct. +- If an existing development clone has an HTTPS `origin`, change it only when requested or when the task explicitly includes remote setup: + + ```sh + git remote set-url origin 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/Guidelines/GitHub/PullRequests.md similarity index 92% rename from AgentGuidelines/Guidelines/GitHub/PullRequests.md rename to Guidelines/GitHub/PullRequests.md index 5010a47..899666f 100644 --- a/AgentGuidelines/Guidelines/GitHub/PullRequests.md +++ b/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/Guidelines/Logging.md b/Guidelines/Logging.md new file mode 100644 index 0000000..498c356 --- /dev/null +++ b/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/Guidelines/Packages.md similarity index 59% rename from AgentGuidelines/Guidelines/Packages.md rename to Guidelines/Packages.md index 8bb04fa..da332c3 100644 --- a/AgentGuidelines/Guidelines/Packages.md +++ b/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/Swift/Localization.md b/Guidelines/Swift/Localization.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/Localization.md rename to Guidelines/Swift/Localization.md diff --git a/AgentGuidelines/Guidelines/Swift/Swift.md b/Guidelines/Swift/Swift.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/Swift.md rename to Guidelines/Swift/Swift.md diff --git a/AgentGuidelines/Guidelines/Swift/SwiftLint.md b/Guidelines/Swift/SwiftLint.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/SwiftLint.md rename to Guidelines/Swift/SwiftLint.md diff --git a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md b/Guidelines/Swift/SwiftStyle.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/SwiftStyle.md rename to Guidelines/Swift/SwiftStyle.md diff --git a/AgentGuidelines/Guidelines/Swift/SwiftUI.md b/Guidelines/Swift/SwiftUI.md similarity index 100% rename from AgentGuidelines/Guidelines/Swift/SwiftUI.md rename to Guidelines/Swift/SwiftUI.md diff --git a/AgentGuidelines/Guidelines/Testing/UnitTesting.md b/Guidelines/Testing/UnitTesting.md similarity index 91% rename from AgentGuidelines/Guidelines/Testing/UnitTesting.md rename to Guidelines/Testing/UnitTesting.md index 4bae316..a694331 100644 --- a/AgentGuidelines/Guidelines/Testing/UnitTesting.md +++ b/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/Guidelines/Xcode/MCP.md b/Guidelines/Xcode/MCP.md similarity index 100% rename from AgentGuidelines/Guidelines/Xcode/MCP.md rename to Guidelines/Xcode/MCP.md diff --git a/AgentGuidelines/Guidelines/Xcode/Security.md b/Guidelines/Xcode/Security.md similarity index 100% rename from AgentGuidelines/Guidelines/Xcode/Security.md rename to Guidelines/Xcode/Security.md diff --git a/LICENSE b/LICENSE index 45f5b45..42d8021 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index f6c0677..0000000 --- a/Package.resolved +++ /dev/null @@ -1,24 +0,0 @@ -{ - "originHash" : "a7cf709b54776e438f795355cd120c1195b4865978d0fb04447d8782f185c8a3", - "pins" : [ - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-plugin", - "state" : { - "revision" : "647c708be89f834fa6a6d4945442793a77ddf5b6", - "version" : "1.5.0" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - } - ], - "version" : 3 -} diff --git a/Package.swift b/Package.swift deleted file mode 100644 index db38baf..0000000 --- a/Package.swift +++ /dev/null @@ -1,31 +0,0 @@ -// swift-tools-version:6.4 - -import PackageDescription - -let package = Package( - name: "ProgressionKit", - platforms: [ - .iOS(.v26), - .macOS(.v26), - .tvOS(.v26), - .watchOS(.v26) - ], - products: [ - .library( - name: "ProgressionKit", - targets: ["ProgressionKit"] - ) - ], - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0") - ], - targets: [ - .target( - name: "ProgressionKit" - ), - .testTarget( - name: "ProgressionKitTests", - dependencies: ["ProgressionKit"] - ) - ] -) diff --git a/README.md b/README.md index fdb244f..9f5c94c 100644 --- a/README.md +++ b/README.md @@ -1,242 +1,132 @@

- Swift - Xcode - Platforms - SPM - DocC + Xcode + Codex + Updated + Revision License - CI - Release + CI

-# ProgressionKit -A reusable progression engine that turns player performance into configurable XP, levels, and unlocks across games and apps. πŸ“ˆ - -`ProgressionKit` is a pure Swift package for apps and games that need deterministic progression logic without coupling progression rules to storage or UI frameworks. - -It models: - -- `XP` gain from successful performance. -- Player levels derived from total XP. -- Track-scoped mastery across distinct content. -- Tier unlocks such as `beginner`, `intermediate`, and `advanced`. - -The package is deliberately content-agnostic. Host apps decide what a track, content item, and tier mean, then feed those identifiers into `ProgressionKit`. - -## Implemented APIs - -- `PKEngine`: applies a progression event to a profile and returns the updated profile plus derived progress values. -- `PKProfile`: persisted progression state for a player. -- `PKConfig`: tunable progression rules such as level size, XP reward, tier order, and unlock thresholds. -- `PKEvent`: a single outcome emitted by the host app. -- `PKUpdate`: the result of applying one event. - -## Structure - -```mermaid -flowchart TB - subgraph HOST["Host App/Game"] - EVENTS["Performance Events"] - STORAGE["Storage Layer"] - UI["UI / HUD / XP Bar"] - end - - subgraph PK[" "] - ENGINE["ProgressionKit"] - PROFILE["PKProfile"] - CONFIG["PKConfig"] - UPDATE["PKUpdate"] - end - - EVENTS --> ENGINE - CONFIG --> ENGINE - ENGINE --> PROFILE - ENGINE --> UPDATE - PROFILE --> STORAGE - UPDATE --> UI +# 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, 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. + +## How it fits together + +```text + thatfactory/agent-guidelines + versioned GitHub repository + | + tagged release + e.g. 0.0.3 + | + git subtree add/pull + | + v ++---------------- Consumer project or package ----------------+ +| | +| AGENTS.md | +| |-- local product/package context | +| |-- concrete project paths | +| |-- local exceptions | +| `-- pointers to shared guidelines -----------------+ | +| | | +| AgentGuidelines/ | | +| |-- VERSION | | +| `-- Guidelines/ <----------------------------------+ | +| |-- Architecture/Redux.md | +| |-- Swift/SwiftUI.md | +| |-- Testing/UnitTesting.md | +| `-- Xcode/MCP.md | +| | +| Sources and project files | ++----------------------------+---------------------------------+ + | + reads instructions and project files + +----------+----------+ + v v + Codex Xcode agent + | + | Xcode MCP (`xcrun mcpbridge`) + v + Xcode ``` -## Quick Start - -Import the package and create an initial player profile: - -```swift -import ProgressionKit - -let profile = PKProfile() -``` - -Create an event whenever the player finishes one unit of content: - -```swift -let event = PKEvent( - contentID: "lesson.greetings.001", - trackID: "japanese-basics", - tierID: "beginner", - wasSuccessful: true -) +The subtree does not automatically import every guide into an agent's context. A consumer's root or folder-scoped `AGENTS.md` tells the agent which shared guides to read for the task. The nearest local `AGENTS.md` can specialize or override the shared baseline. + +## Guideline catalog + +- [Redux architecture and physical folder organization](Guidelines/Architecture/Redux.md) +- [Swift](Guidelines/Swift/Swift.md) +- [Swift style](Guidelines/Swift/SwiftStyle.md) +- [SwiftUI](Guidelines/Swift/SwiftUI.md) +- [SwiftLint](Guidelines/Swift/SwiftLint.md) +- [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, logging, packages, CI/CD, and Xcode guidance, but not Redux or SwiftUI guidance. + +## Add to a consumer + +From the consumer repository root, install a tagged release: + +```sh +git subtree add \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + 0.0.7 \ + --squash ``` -Apply the event to the profile: - -```swift -let update = PKEngine.apply( - event: event, - to: profile -) -``` - -`update` is a `PKUpdate` value that contains the updated `PKProfile` and derived progression values your app can render immediately. - -Common `PKUpdate` values you will typically use: - -- `update.profile`: persist this as the new `PKProfile`. -- `update.playerLevel`: current player level. -- `update.xpIntoLevel` and `update.xpForNextLevel`: useful for progress bars. -- `update.newlyUnlockedTierIDs`: tiers unlocked by the latest event. -- `update.didGrantXP`: whether the event changed XP. - -## Configure Progression Rules - -Use `PKConfig` when you want to customize level size, XP rewards, tier unlock order, and the mastery requirement for unlocking the next tier: +Copy and adapt [the consumer template](Templates/AGENTS.md). Keep the consumer file small: describe the product or package, map its concrete physical folders, point to the applicable shared guides, and state only genuine exceptions. -```swift -let config = PKConfig( - levelXP: 120, - masteryXP: 15, - tierOrder: ["beginner", "intermediate", "advanced"], - masteryRequirement: 4 -) -``` +## Update a consumer -Apply the same event with your custom config: +Review the target release's changelog, then pull it deliberately: -```swift -let configuredUpdate = PKEngine.apply( - event: event, - to: profile, - config: config -) +```sh +git subtree pull \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + 0.0.7 \ + --squash ``` -In practice: - -- Persist `configuredUpdate.profile` (your new `PKProfile`) after each event. -- Read other `PKUpdate` values to update your UI (XP gain, level changes, unlock state, and mastery). - -## SwiftUI Example (Simple Progress Bar) - -This example shows a simple integration pattern: apply progression events, keep the latest `PKUpdate`, and render a progress bar from the returned values. - -### Video - -https://github.com/user-attachments/assets/3920bbde-7b6b-40f6-b02f-f5506410b4fb - -### Code - -```swift -import ProgressionKit -import SwiftUI - -struct ProgressionDemoView: View { - @State private var profile = PKProfile() - @State private var lessonNumber = 1 - - private let config = PKConfig() - - private var progress: Double { - min(Double(profile.totalXP) / Double(config.levelXP), 1) - } - - var body: some View { - VStack(spacing: 16) { - Text(progress < 1 ? "Level 1" : "Level 2 πŸ₯³") - .font(.headline) +Confirm `AgentGuidelines/VERSION`, review the subtree diff, validate local `AGENTS.md` pointers, and run the consumer's relevant tests. Updates are intentionally not automatic: one guideline release cannot silently change every project. - GeometryReader { geometry in - let totalWidth = geometry.size.width - let fillWidth = totalWidth * progress +## Maintain the source of truth - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 10) - .fill(.gray.opacity(0.25)) +1. Export current Xcode skills to a temporary review location when a new Xcode release materially changes agent behavior: - RoundedRectangle(cornerRadius: 10) - .fill(.green) - .frame(width: fillWidth) - .animation(.snappy, value: progress) - } - } - .frame(height: 16) + ```sh + xcrun agent skills export --output-dir + ``` - Text("\(Int(progress * 100))%") - .font(.caption) - .foregroundStyle(.secondary) +2. Compare relevant guidance with this repository and official Apple documentation. +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`, open a pull request, and wait for approval before merging. +7. After the pull request has merged, create the matching tag and GitHub release. - Button("Complete Lesson") { - let event = PKEvent( - contentID: "lesson.greetings.\(lessonNumber)", - trackID: "japanese-basics", - tierID: "beginner", - wasSuccessful: true - ) +## Precedence - let update = PKEngine.apply( - event: event, - to: profile, - config: config - ) +For a consumer task, apply instructions in this order: - withAnimation(.snappy) { - profile = update.profile - } - lessonNumber += 1 - } - } - .padding() - } -} - -// MARK: - Preview - -#Preview { - ProgressionDemoView() -} -``` - -## Integration - -### Xcode -Use Xcode's [built-in support for SPM](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). - -*or...* - -### Package.swift -In your `Package.swift`, add `ProgressionKit` as a dependency: - -```swift -dependencies: [ - .package( - url: "https://github.com/thatfactory/progressionkit", - from: "0.1.3" - ) -] -``` - -Associate the dependency with your target: - -```swift -targets: [ - .target( - name: "YourTarget", - dependencies: [ - .product( - name: "ProgressionKit", - package: "progressionkit" - ) - ] - ) -] -``` +1. The user's explicit request. +2. The nearest applicable consumer `AGENTS.md`. +3. The consumer root `AGENTS.md`. +4. The shared guides explicitly referenced by those files. -Run: `swift build` +Official Apple documentation remains authoritative for API behavior. A local convention can deliberately narrow a choice, but it must not rely on behavior contradicted by the current SDK documentation. diff --git a/AgentGuidelines/Scripts/validate_guidelines.py b/Scripts/validate_guidelines.py similarity index 100% rename from AgentGuidelines/Scripts/validate_guidelines.py rename to Scripts/validate_guidelines.py diff --git a/Sources/ProgressionKit/PKConfig.swift b/Sources/ProgressionKit/PKConfig.swift deleted file mode 100644 index 10044a7..0000000 --- a/Sources/ProgressionKit/PKConfig.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -/// Defines the rules that control XP gain, levels, and tier unlocks. -public struct PKConfig: Equatable, Codable, Sendable { - /// The amount of XP needed for each player level. - public let levelXP: Int - - /// The XP awarded when a content item grants mastery for the first time. - public let masteryXP: Int - - /// The ordered tier identifiers used to unlock more difficult content. - public let tierOrder: [String] - - /// The number of distinct mastered content items required to unlock the next tier. - public let masteryRequirement: Int - - /// Creates a progression configuration. - /// - /// - Parameters: - /// - levelXP: The amount of XP needed for each player level. - /// - masteryXP: The XP awarded when a content item grants mastery for the first time. - /// - tierOrder: The ordered tier identifiers used to unlock more difficult content. - /// - masteryRequirement: The number of distinct mastered content items required to unlock the next tier. - public init( - levelXP: Int = 100, - masteryXP: Int = 10, - tierOrder: [String] = ["beginner", "intermediate", "advanced"], - masteryRequirement: Int = 5 - ) { - precondition(levelXP > 0, "levelXP must be greater than zero.") - precondition(masteryXP >= 0, "masteryXP must be zero or greater.") - precondition(!tierOrder.isEmpty, "tierOrder must not be empty.") - precondition(masteryRequirement > 0, "masteryRequirement must be greater than zero.") - - self.levelXP = levelXP - self.masteryXP = masteryXP - self.tierOrder = tierOrder - self.masteryRequirement = masteryRequirement - } -} - -// MARK: - Defaults - -extension PKConfig { - /// The default configuration for tiered progression systems. - public static let standard = PKConfig() -} diff --git a/Sources/ProgressionKit/PKEngine.swift b/Sources/ProgressionKit/PKEngine.swift deleted file mode 100644 index a0b241e..0000000 --- a/Sources/ProgressionKit/PKEngine.swift +++ /dev/null @@ -1,115 +0,0 @@ -import Foundation - -/// Applies progression events to profiles using a deterministic rule set. -public enum PKEngine { - /// Applies one progression event to a player profile. - /// - /// - Parameters: - /// - event: The event emitted by the host app. - /// - profile: The player profile to update. - /// - config: The progression rule set to apply. - /// - Returns: The updated profile and its derived values. - public static func apply( - event: PKEvent, - to profile: PKProfile, - config: PKConfig = .standard - ) -> PKUpdate { - var updatedProfile = profile - var trackProgress = updatedProfile.trackProgress[event.trackID] ?? defaultTrackProgress(config: config) - let isTierUnlocked = trackProgress.unlockedTierIDs.contains(event.tierID) - - guard event.wasSuccessful, isTierUnlocked else { - updatedProfile.trackProgress[event.trackID] = trackProgress - return makeUpdate( - profile: updatedProfile, - config: config, - didGrantXP: false, - newlyUnlockedTierIDs: [] - ) - } - - var tierProgress = trackProgress.tierProgress[event.tierID] ?? PKTierProgress() - guard !tierProgress.masteredContentIDs.contains(event.contentID) else { - updatedProfile.trackProgress[event.trackID] = trackProgress - return makeUpdate( - profile: updatedProfile, - config: config, - didGrantXP: false, - newlyUnlockedTierIDs: [] - ) - } - - tierProgress.masteredContentIDs.insert(event.contentID) - trackProgress.tierProgress[event.tierID] = tierProgress - updatedProfile.totalXP += config.masteryXP - - let newlyUnlockedTierIDs = unlockNextTierIfNeeded( - trackProgress: &trackProgress, - tierID: event.tierID, - config: config - ) - - updatedProfile.trackProgress[event.trackID] = trackProgress - - return makeUpdate( - profile: updatedProfile, - config: config, - didGrantXP: true, - newlyUnlockedTierIDs: newlyUnlockedTierIDs - ) - } -} - -// MARK: - Private - -private extension PKEngine { - static func defaultTrackProgress(config: PKConfig) -> PKTrackProgress { - let firstTierID = config.tierOrder[0] - - return PKTrackProgress( - unlockedTierIDs: [firstTierID] - ) - } - - static func unlockNextTierIfNeeded( - trackProgress: inout PKTrackProgress, - tierID: String, - config: PKConfig - ) -> [String] { - guard - let currentIndex = config.tierOrder.firstIndex(of: tierID), - currentIndex < config.tierOrder.count - 1, - let tierProgress = trackProgress.tierProgress[tierID], - tierProgress.masteredContentIDs.count >= config.masteryRequirement - else { - return [] - } - - let nextTierID = config.tierOrder[currentIndex + 1] - guard !trackProgress.unlockedTierIDs.contains(nextTierID) else { - return [] - } - - trackProgress.unlockedTierIDs.insert(nextTierID) - return [nextTierID] - } - - static func makeUpdate( - profile: PKProfile, - config: PKConfig, - didGrantXP: Bool, - newlyUnlockedTierIDs: [String] - ) -> PKUpdate { - let playerLevel = (profile.totalXP / config.levelXP) + 1 - let xpIntoLevel = profile.totalXP % config.levelXP - - return PKUpdate( - didGrantXP: didGrantXP, - newlyUnlockedTierIDs: newlyUnlockedTierIDs, - playerLevel: playerLevel, - profile: profile, - xpForNextLevel: config.levelXP, - xpIntoLevel: xpIntoLevel - ) - } -} diff --git a/Sources/ProgressionKit/PKEvent.swift b/Sources/ProgressionKit/PKEvent.swift deleted file mode 100644 index dc1d24c..0000000 --- a/Sources/ProgressionKit/PKEvent.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -/// Represents one gameplay outcome that can affect player progression. -public struct PKEvent: Equatable, Codable, Sendable { - /// The stable identifier for the content item that was attempted. - public let contentID: String - - /// The stable identifier for the track this content belongs to. - public let trackID: String - - /// The tier identifier for the attempted content. - public let tierID: String - - /// Indicates whether the attempt should grant progression credit. - public let wasSuccessful: Bool - - /// Creates a progression event. - /// - /// - Parameters: - /// - contentID: The stable identifier for the content item that was attempted. - /// - trackID: The stable identifier for the track this content belongs to. - /// - tierID: The tier identifier for the attempted content. - /// - wasSuccessful: Indicates whether the attempt should grant progression credit. - public init( - contentID: String, - trackID: String, - tierID: String, - wasSuccessful: Bool - ) { - self.contentID = contentID - self.trackID = trackID - self.tierID = tierID - self.wasSuccessful = wasSuccessful - } -} diff --git a/Sources/ProgressionKit/PKProfile.swift b/Sources/ProgressionKit/PKProfile.swift deleted file mode 100644 index d03acb3..0000000 --- a/Sources/ProgressionKit/PKProfile.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// Stores the persisted progression state for one player. -public struct PKProfile: Equatable, Codable, Sendable { - /// The player's cumulative XP across all tracks. - public var totalXP: Int - - /// The player's progression grouped by track identifier. - public var trackProgress: [String: PKTrackProgress] - - /// Creates a player progression profile. - /// - /// - Parameters: - /// - totalXP: The player's cumulative XP across all tracks. - /// - trackProgress: The player's progression grouped by track identifier. - public init( - totalXP: Int = 0, - trackProgress: [String: PKTrackProgress] = [:] - ) { - self.totalXP = totalXP - self.trackProgress = trackProgress - } -} diff --git a/Sources/ProgressionKit/PKTierProgress.swift b/Sources/ProgressionKit/PKTierProgress.swift deleted file mode 100644 index 6b2d0f9..0000000 --- a/Sources/ProgressionKit/PKTierProgress.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -/// Stores mastery information for a single tier within a track. -public struct PKTierProgress: Equatable, Codable, Sendable { - /// The distinct content identifiers that already granted mastery credit. - public var masteredContentIDs: Set - - /// Creates tier progress state. - /// - /// - Parameter masteredContentIDs: The distinct content identifiers that already granted mastery credit. - public init(masteredContentIDs: Set = []) { - self.masteredContentIDs = masteredContentIDs - } -} diff --git a/Sources/ProgressionKit/PKTrackProgress.swift b/Sources/ProgressionKit/PKTrackProgress.swift deleted file mode 100644 index 31962e6..0000000 --- a/Sources/ProgressionKit/PKTrackProgress.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// Stores progression for one track, including unlocked tiers and mastery history. -public struct PKTrackProgress: Equatable, Codable, Sendable { - /// The set of tiers currently unlocked for this track. - public var unlockedTierIDs: Set - - /// The mastery state grouped by tier identifier. - public var tierProgress: [String: PKTierProgress] - - /// Creates track progress state. - /// - /// - Parameters: - /// - unlockedTierIDs: The set of tiers currently unlocked for this track. - /// - tierProgress: The mastery state grouped by tier identifier. - public init( - unlockedTierIDs: Set = [], - tierProgress: [String: PKTierProgress] = [:] - ) { - self.unlockedTierIDs = unlockedTierIDs - self.tierProgress = tierProgress - } -} diff --git a/Sources/ProgressionKit/PKUpdate.swift b/Sources/ProgressionKit/PKUpdate.swift deleted file mode 100644 index 2b54e4d..0000000 --- a/Sources/ProgressionKit/PKUpdate.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -/// Describes the result of applying one progression event. -public struct PKUpdate: Equatable, Codable, Sendable { - /// Indicates whether the event granted new XP. - public let didGrantXP: Bool - - /// The tiers unlocked by this event, if any. - public let newlyUnlockedTierIDs: [String] - - /// The player's current level derived from total XP. - public let playerLevel: Int - - /// The updated profile after applying the event. - public let profile: PKProfile - - /// The XP required to complete the current level. - public let xpForNextLevel: Int - - /// The amount of XP already earned within the current level. - public let xpIntoLevel: Int - - /// Creates a progression update. - /// - /// - Parameters: - /// - didGrantXP: Indicates whether the event granted new XP. - /// - newlyUnlockedTierIDs: The tiers unlocked by this event, if any. - /// - playerLevel: The player's current level derived from total XP. - /// - profile: The updated profile after applying the event. - /// - xpForNextLevel: The XP required to complete the current level. - /// - xpIntoLevel: The amount of XP already earned within the current level. - public init( - didGrantXP: Bool, - newlyUnlockedTierIDs: [String], - playerLevel: Int, - profile: PKProfile, - xpForNextLevel: Int, - xpIntoLevel: Int - ) { - self.didGrantXP = didGrantXP - self.newlyUnlockedTierIDs = newlyUnlockedTierIDs - self.playerLevel = playerLevel - self.profile = profile - self.xpForNextLevel = xpForNextLevel - self.xpIntoLevel = xpIntoLevel - } -} diff --git a/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md b/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md deleted file mode 100644 index f8f659b..0000000 --- a/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md +++ /dev/null @@ -1,67 +0,0 @@ -# ``ProgressionKit`` - -Deterministic progression logic for XP, levels, and tier unlocks. - -@Metadata { - @Available(iOS, introduced: "26.0") - @Available(macOS, introduced: "26.0") - @Available(tvOS, introduced: "26.0") - @Available(watchOS, introduced: "26.0") - @Available(visionOS, introduced: "26.0") -} - -## Overview - -`ProgressionKit` is a pure Swift package for apps and games that need deterministic progression logic without coupling progression rules to storage or UI frameworks. - -It models XP gain from successful performance, player levels derived from total XP, track-scoped mastery across distinct content, and tier unlocks such as `beginner`, `intermediate`, and `advanced`. - -The package is content-agnostic. Host apps decide what a track, content item, and tier mean, then feed those identifiers into ``PKEngine``. - -## Usage - -```swift -import ProgressionKit - -let profile = PKProfile() -let event = PKEvent( - contentID: "A11IYR-CE4D7B84", - trackID: "A11IYR", - tierID: "beginner", - wasSuccessful: true -) - -let update = PKEngine.apply( - event: event, - to: profile -) -``` - -```swift -let config = PKConfig( - levelXP: 120, - masteryXP: 15, - tierOrder: ["bronze", "silver", "gold"], - masteryRequirement: 4 -) - -let tunedUpdate = PKEngine.apply( - event: event, - to: profile, - config: config -) -``` - -## Topics - -### Core Types - -Use ``PKProfile`` to store player progression, ``PKEvent`` to represent one gameplay outcome, and ``PKUpdate`` to read the derived result after applying an event. - -### Engine - -Use ``PKEngine/apply(event:to:config:)`` to apply progression rules synchronously and deterministically. - -### Configuration - -Use ``PKConfig`` to tune level size, mastery XP, tier order, and the unlock threshold for your app or game. diff --git a/Sources/ProgressionKit/ProgressionKit.swift b/Sources/ProgressionKit/ProgressionKit.swift deleted file mode 100644 index fecc4ab..0000000 --- a/Sources/ProgressionKit/ProgressionKit.swift +++ /dev/null @@ -1 +0,0 @@ -import Foundation diff --git a/AgentGuidelines/Templates/AGENTS.md b/Templates/AGENTS.md similarity index 97% rename from AgentGuidelines/Templates/AGENTS.md rename to Templates/AGENTS.md index b28c118..66d89dd 100644 --- a/AgentGuidelines/Templates/AGENTS.md +++ b/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/Tests/ProgressionKitTests/ProgressionKitTests.swift b/Tests/ProgressionKitTests/ProgressionKitTests.swift deleted file mode 100644 index ed99a38..0000000 --- a/Tests/ProgressionKitTests/ProgressionKitTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -import Testing -@testable import ProgressionKit - -@Test func grantsXPForFirstSuccessfulCompletion() { - // Given - let event = PKEvent( - contentID: "lesson-1", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: true - ) - - // When - let update = PKEngine.apply( - event: event, - to: PKProfile() - ) - - // Then - #expect(update.didGrantXP) - #expect(update.profile.totalXP == 10) - #expect(update.playerLevel == 1) - #expect(update.xpIntoLevel == 10) -} - -@Test func doesNotGrantXPForRepeatedSuccessfulCompletion() { - // Given - let event = PKEvent( - contentID: "lesson-1", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: true - ) - let firstUpdate = PKEngine.apply( - event: event, - to: PKProfile() - ) - - // When - let secondUpdate = PKEngine.apply( - event: event, - to: firstUpdate.profile - ) - - // Then - #expect(!secondUpdate.didGrantXP) - #expect(secondUpdate.profile.totalXP == 10) -} - -@Test func doesNotGrantXPForIncorrectAttempts() { - // Given - let event = PKEvent( - contentID: "lesson-1", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: false - ) - - // When - let update = PKEngine.apply( - event: event, - to: PKProfile() - ) - - // Then - #expect(!update.didGrantXP) - #expect(update.profile.totalXP == 0) -} - -@Test func unlocksIntermediateAfterDistinctBeginnerMastery() { - // Given - let events = (1 ... 5).map { index in - PKEvent( - contentID: "lesson-\(index)", - trackID: "a11-reading", - tierID: "beginner", - wasSuccessful: true - ) - } - - // When - let finalUpdate = events.reduce( - PKUpdate( - didGrantXP: false, - newlyUnlockedTierIDs: [], - playerLevel: 1, - profile: PKProfile(), - xpForNextLevel: 100, - xpIntoLevel: 0 - ) - ) { partialUpdate, event in - PKEngine.apply( - event: event, - to: partialUpdate.profile - ) - } - - // Then - #expect(finalUpdate.newlyUnlockedTierIDs == ["intermediate"]) - #expect( - finalUpdate.profile.trackProgress["a11-reading"]?.unlockedTierIDs.contains("intermediate") == true - ) -} - -@Test func unlocksAdvancedAfterDistinctIntermediateMastery() { - // Given - let beginnerEvents = (1 ... 5).map { index in - PKEvent( - contentID: "beginner-\(index)", - trackID: "a11-writing", - tierID: "beginner", - wasSuccessful: true - ) - } - let intermediateEvents = (1 ... 5).map { index in - PKEvent( - contentID: "intermediate-\(index)", - trackID: "a11-writing", - tierID: "intermediate", - wasSuccessful: true - ) - } - - let unlockedIntermediateProfile = beginnerEvents.reduce(PKProfile()) { profile, event in - PKEngine.apply( - event: event, - to: profile - ).profile - } - - // When - let finalUpdate = intermediateEvents.reduce( - PKUpdate( - didGrantXP: false, - newlyUnlockedTierIDs: [], - playerLevel: 1, - profile: unlockedIntermediateProfile, - xpForNextLevel: 100, - xpIntoLevel: 0 - ) - ) { partialUpdate, event in - PKEngine.apply( - event: event, - to: partialUpdate.profile - ) - } - - // Then - #expect(finalUpdate.newlyUnlockedTierIDs == ["advanced"]) - #expect( - finalUpdate.profile.trackProgress["a11-writing"]?.unlockedTierIDs.contains("advanced") == true - ) -} - -@Test func derivesLevelAcrossBoundary() { - // Given - let config = PKConfig( - levelXP: 100, - masteryXP: 25, - tierOrder: ["beginner", "intermediate", "advanced"], - masteryRequirement: 5 - ) - let events = (1 ... 4).map { index in - PKEvent( - contentID: "lesson-\(index)", - trackID: "a11-listening", - tierID: "beginner", - wasSuccessful: true - ) - } - - // When - let finalProfile = events.reduce(PKProfile()) { profile, event in - PKEngine.apply( - event: event, - to: profile, - config: config - ).profile - } - let finalUpdate = PKEngine.apply( - event: PKEvent( - contentID: "lesson-5", - trackID: "a11-listening", - tierID: "beginner", - wasSuccessful: false - ), - to: finalProfile, - config: config - ) - - // Then - #expect(finalUpdate.profile.totalXP == 100) - #expect(finalUpdate.playerLevel == 2) - #expect(finalUpdate.xpIntoLevel == 0) - #expect(finalUpdate.xpForNextLevel == 100) -} diff --git a/AgentGuidelines/Tests/test_validate_guidelines.py b/Tests/test_validate_guidelines.py similarity index 100% rename from AgentGuidelines/Tests/test_validate_guidelines.py rename to Tests/test_validate_guidelines.py diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..5a5831a --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.7 From b2e0883ad77704ea3729d1f7b009e48c955be7c7 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Thu, 23 Jul 2026 17:14:58 +0200 Subject: [PATCH 2/2] Log ProgressionKit progression outcomes --- AGENTS.md | 2 + Package.resolved | 11 +- Package.swift | 17 ++- README.md | 8 +- Sources/ProgressionKit/PKEngine.swift | 9 +- Sources/ProgressionKit/PKLogging.swift | 42 ++++++++ .../ProgressionKit.docc/ProgressionKit.md | 2 + .../ProgressionKitTests/PKLoggingTests.swift | 100 ++++++++++++++++++ 8 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 Sources/ProgressionKit/PKLogging.swift create mode 100644 Tests/ProgressionKitTests/PKLoggingTests.swift 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/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 +}