From d31c4e249ed3775f29c3cd4db26fafe73252e915 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Thu, 23 Jul 2026 18:11:48 +0200 Subject: [PATCH 1/2] Collapse synchronized guideline diffs --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..38ec4db --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. +AgentGuidelines/** linguist-generated From 3ac0741f01edfbd5745c041d21979490fd5ff382 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Thu, 23 Jul 2026 18:12:04 +0200 Subject: [PATCH 2/2] Squashed 'AgentGuidelines/' changes from c037349..0146279 0146279 Collapse synchronized guideline diffs in consumer PRs (#9) git-subtree-dir: AgentGuidelines git-subtree-split: 0146279ee26258baa0738dcce337a955d2690a9e --- .github/workflows/ci.yml | 32 +- .github/workflows/nightly.yml | 31 -- .github/workflows/release.yml | 125 ++----- .gitignore | 12 +- AGENTS.md | 75 ++-- AgentGuidelines/.github/workflows/ci.yml | 23 -- AgentGuidelines/.github/workflows/release.yml | 44 --- AgentGuidelines/.gitignore | 3 - AgentGuidelines/AGENTS.md | 54 --- AgentGuidelines/LICENSE | 22 -- AgentGuidelines/README.md | 132 ------- AgentGuidelines/VERSION | 1 - AgentGuidelines/CHANGELOG.md => CHANGELOG.md | 7 + .../Architecture/Redux.md | 0 .../Guidelines => Guidelines}/CICD.md | 0 .../Guidelines => Guidelines}/Development.md | 11 + .../Documentation.md | 0 .../Git/Repositories.md | 0 .../GitHub/PullRequests.md | 0 .../Guidelines => Guidelines}/Logging.md | 0 .../Guidelines => Guidelines}/Packages.md | 0 .../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 | 0 .../Guidelines => Guidelines}/Xcode/MCP.md | 0 .../Xcode/Security.md | 0 LICENSE | 1 + Package.resolved | 33 -- Package.swift | 44 --- README.md | 339 ++++++------------ .../validate_guidelines.py | 1 + Sources/ProgressionKit/PKConfig.swift | 47 --- Sources/ProgressionKit/PKEngine.swift | 122 ------- Sources/ProgressionKit/PKEvent.swift | 35 -- Sources/ProgressionKit/PKLogging.swift | 42 --- Sources/ProgressionKit/PKProfile.swift | 23 -- Sources/ProgressionKit/PKTierProgress.swift | 14 - Sources/ProgressionKit/PKTrackProgress.swift | 23 -- Sources/ProgressionKit/PKUpdate.swift | 47 --- .../ProgressionKit.docc/ProgressionKit.md | 69 ---- Sources/ProgressionKit/ProgressionKit.swift | 1 - .../Templates => Templates}/AGENTS.md | 0 .../ProgressionKitTests/PKLoggingTests.swift | 100 ------ .../ProgressionKitTests.swift | 196 ---------- .../test_validate_guidelines.py | 0 VERSION | 1 + 49 files changed, 222 insertions(+), 1488 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/LICENSE delete mode 100644 AgentGuidelines/README.md delete mode 100644 AgentGuidelines/VERSION rename AgentGuidelines/CHANGELOG.md => CHANGELOG.md (88%) rename {AgentGuidelines/Guidelines => Guidelines}/Architecture/Redux.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/CICD.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Development.md (54%) rename {AgentGuidelines/Guidelines => Guidelines}/Documentation.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Git/Repositories.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/GitHub/PullRequests.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Logging.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Packages.md (100%) 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 (100%) 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 (98%) 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/PKLogging.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 (100%) delete mode 100644 Tests/ProgressionKitTests/PKLoggingTests.swift 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 0366cb3..f3cc3d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,43 +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) -- [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) -- [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. -- Use logging subsystem `com.thatfactory.progressionkit`, category `progression`, and canonical package emoji `📈`. +```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 f3cc3d6..0000000 --- a/AgentGuidelines/AGENTS.md +++ /dev/null @@ -1,54 +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. -- Before changing this repository, verify that the consumer's checked-in guidelines version is current where applicable. - -## 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. -- When releasing a new version, update the version in both the README installation command and the README consumer-update command. Keep both commands aligned with the new release, for example: - - ```sh - git subtree add \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - \ - --squash - - git subtree pull \ - --prefix=AgentGuidelines \ - https://github.com/thatfactory/agent-guidelines.git \ - \ - --squash - ``` - -## Validation - -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/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 9f5c94c..0000000 --- a/AgentGuidelines/README.md +++ /dev/null @@ -1,132 +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, 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 -``` - -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 -``` - -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 \ - 0.0.7 \ - --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`, open a pull request, and wait for approval before merging. -7. After the pull request has merged, create the matching tag and GitHub release. - -## Precedence - -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 5a5831a..0000000 --- a/AgentGuidelines/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.7 diff --git a/AgentGuidelines/CHANGELOG.md b/CHANGELOG.md similarity index 88% rename from AgentGuidelines/CHANGELOG.md rename to CHANGELOG.md index faa1a6b..a190b2d 100644 --- a/AgentGuidelines/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project are documented in this file. +## [0.0.8] - 2026-07-23 + +### Added + +- Consumer guidance for keeping `AgentGuidelines/` tracked while collapsing synchronized files in GitHub pull-request diffs with `.gitattributes`. +- Pull-request conventions for isolated subtree commits, explicit version notes, central review links, and continued CI validation. + ## [0.0.7] - 2026-07-23 ### Added diff --git a/AgentGuidelines/Guidelines/Architecture/Redux.md b/Guidelines/Architecture/Redux.md similarity index 100% rename from AgentGuidelines/Guidelines/Architecture/Redux.md rename to Guidelines/Architecture/Redux.md diff --git a/AgentGuidelines/Guidelines/CICD.md b/Guidelines/CICD.md similarity index 100% rename from AgentGuidelines/Guidelines/CICD.md rename to Guidelines/CICD.md diff --git a/AgentGuidelines/Guidelines/Development.md b/Guidelines/Development.md similarity index 54% rename from AgentGuidelines/Guidelines/Development.md rename to Guidelines/Development.md index 8ab764d..afce49f 100644 --- a/AgentGuidelines/Guidelines/Development.md +++ b/Guidelines/Development.md @@ -8,6 +8,17 @@ When developing a new feature or responding to a feature request, consider share 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. +## Guidelines changes in pull requests + +Keep `AgentGuidelines/` tracked so consumers retain a reproducible, versioned copy for agents and CI. Do not add the subtree to `.gitignore`. Instead, add this rule to the consumer's tracked `.gitattributes` so GitHub collapses synchronized guideline files in pull-request diffs by default while reviewers can still expand them: + +```gitattributes +# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. +AgentGuidelines/** linguist-generated +``` + +Keep each subtree update in its own commit. In the pull-request description, state the old and new guideline versions and link to the central release or pull request where the guideline changes were reviewed. Continue validating the checked-in subtree in CI. Because generated-file diffs are collapsed by default, never edit the subtree locally; make shared changes in the source repository and consume a tagged release. + ## 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 100% rename from AgentGuidelines/Guidelines/Documentation.md rename to Guidelines/Documentation.md diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/Guidelines/Git/Repositories.md similarity index 100% rename from AgentGuidelines/Guidelines/Git/Repositories.md rename to Guidelines/Git/Repositories.md diff --git a/AgentGuidelines/Guidelines/GitHub/PullRequests.md b/Guidelines/GitHub/PullRequests.md similarity index 100% rename from AgentGuidelines/Guidelines/GitHub/PullRequests.md rename to Guidelines/GitHub/PullRequests.md diff --git a/AgentGuidelines/Guidelines/Logging.md b/Guidelines/Logging.md similarity index 100% rename from AgentGuidelines/Guidelines/Logging.md rename to Guidelines/Logging.md diff --git a/AgentGuidelines/Guidelines/Packages.md b/Guidelines/Packages.md similarity index 100% rename from AgentGuidelines/Guidelines/Packages.md rename to Guidelines/Packages.md 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 100% rename from AgentGuidelines/Guidelines/Testing/UnitTesting.md rename to Guidelines/Testing/UnitTesting.md 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 06507de..0000000 --- a/Package.resolved +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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", - "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 20faee8..0000000 --- a/Package.swift +++ /dev/null @@ -1,44 +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/thatfactory/applogger", from: "1.1.0"), - .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0") - ], - targets: [ - .target( - name: "ProgressionKit", - dependencies: [ - .product( - name: "AppLogger", - package: "applogger" - ) - ] - ), - .testTarget( - name: "ProgressionKitTests", - dependencies: [ - "ProgressionKit", - .product( - name: "AppLogger", - package: "applogger" - ) - ] - ) - ] -) diff --git a/README.md b/README.md index 10f4e18..c40ea35 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,139 @@

- 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`. - -## 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. -- `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() +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.8 \ + --squash ``` -Create an event whenever the player finishes one unit of content: +Keep the subtree tracked, but add this to the consumer's tracked `.gitattributes` so GitHub collapses synchronized guideline files in pull-request diffs by default: -```swift -let event = PKEvent( - contentID: "lesson.greetings.001", - trackID: "japanese-basics", - tierID: "beginner", - wasSuccessful: true -) +```gitattributes +# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. +AgentGuidelines/** linguist-generated ``` -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.8 \ + --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`, ensure the `.gitattributes` rule above is present, review the subtree diff, validate local `AGENTS.md` pointers, and run the consumer's relevant tests. Keep the subtree update in its own commit, and identify the old and new versions plus the central release or pull request in the consumer pull-request description. 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.4" - ) -] -``` - -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 98% rename from AgentGuidelines/Scripts/validate_guidelines.py rename to Scripts/validate_guidelines.py index 3b76c8a..b816c50 100644 --- a/AgentGuidelines/Scripts/validate_guidelines.py +++ b/Scripts/validate_guidelines.py @@ -86,6 +86,7 @@ def validate_readme_contract(errors: list[str]) -> None: "https://github.com/thatfactory/agent-guidelines.git": "subtree remote", "git subtree add": "subtree installation command", "git subtree pull": "subtree update command", + "AgentGuidelines/** linguist-generated": "generated subtree attribute", } for value, description in required.items(): if value not in readme: 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 0873954..0000000 --- a/Sources/ProgressionKit/PKEngine.swift +++ /dev/null @@ -1,122 +0,0 @@ -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. - /// - /// - 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 - - let update = PKUpdate( - didGrantXP: didGrantXP, - newlyUnlockedTierIDs: newlyUnlockedTierIDs, - playerLevel: playerLevel, - profile: profile, - xpForNextLevel: config.levelXP, - xpIntoLevel: xpIntoLevel - ) - PKLogging.logProgression( - update: update, - xpGranted: didGrantXP ? config.masteryXP : 0 - ) - return update - } -} 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/PKLogging.swift b/Sources/ProgressionKit/PKLogging.swift deleted file mode 100644 index 5357884..0000000 --- a/Sources/ProgressionKit/PKLogging.swift +++ /dev/null @@ -1,42 +0,0 @@ -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/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 0754f83..0000000 --- a/Sources/ProgressionKit/ProgressionKit.docc/ProgressionKit.md +++ /dev/null @@ -1,69 +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``. - -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 -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 100% rename from AgentGuidelines/Templates/AGENTS.md rename to Templates/AGENTS.md diff --git a/Tests/ProgressionKitTests/PKLoggingTests.swift b/Tests/ProgressionKitTests/PKLoggingTests.swift deleted file mode 100644 index ef2945f..0000000 --- a/Tests/ProgressionKitTests/PKLoggingTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -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 -} 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..d169b2f --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.8