From 44bd8436588225b92b0cd10b1de5f3cdd45feb61 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Thu, 13 Aug 2026 22:16:05 +0200 Subject: [PATCH 1/2] Squashed 'AgentGuidelines/' changes from edcf0b2..1e6127b 1e6127b Clarify Swift source structure guidance (#20) 2b1f446 Avoid manual Codex review requests (#19) 5b8940b Add shared agent workflow guidance (#18) 8cc9327 Remove obsolete Store isolation workarounds (#17) 64ac63d Add completion audit and refine Swift guidance (#16) d959548 Document login-shell GitHub CLI credentials (#15) 38f4736 Run swift-format before compilation (#14) 2152b66 Update README.md (#13) 28c76ae Update README.md (#12) ce9f351 Adopt shared swift-format configuration (#11) git-subtree-dir: AgentGuidelines git-subtree-split: 1e6127b8eea820b2913a536b966f406e8e9bcd6d --- .../skills/agent-guidelines-audit/SKILL.md | 60 +++ .../agent-guidelines-audit/agents/openai.yaml | 4 + .gitattributes | 2 - .github/workflows/ci.yml | 32 +- .github/workflows/nightly.yml | 31 -- .github/workflows/release.yml | 125 ++----- .gitignore | 12 +- AGENTS.md | 81 ++-- AgentGuidelines/.github/workflows/ci.yml | 23 -- AgentGuidelines/.github/workflows/release.yml | 44 --- AgentGuidelines/.gitignore | 3 - AgentGuidelines/AGENTS.md | 58 --- AgentGuidelines/CHANGELOG.md | 78 ---- AgentGuidelines/Guidelines/Swift/SwiftLint.md | 8 - .../Guidelines/Swift/SwiftStyle.md | 25 -- AgentGuidelines/LICENSE | 22 -- AgentGuidelines/README.md | 139 ------- .../Scripts/validate_guidelines.py | 125 ------- .../Tests/test_validate_guidelines.py | 52 --- AgentGuidelines/VERSION | 1 - CHANGELOG.md | 149 ++++++++ Configurations/Swift/.editorconfig | 10 + Configurations/Swift/.swift-format | 81 ++++ Guidelines/AgentWorkflow.md | 49 +++ .../Architecture/Redux.md | 97 +++-- .../Guidelines => Guidelines}/CICD.md | 0 .../Guidelines => Guidelines}/Development.md | 6 + .../Documentation.md | 3 +- .../Git/Repositories.md | 10 + .../GitHub/PullRequests.md | 1 + .../Guidelines => Guidelines}/Logging.md | 0 .../Guidelines => Guidelines}/Packages.md | 0 .../Swift/Localization.md | 0 .../Guidelines => Guidelines}/Swift/Swift.md | 6 +- Guidelines/Swift/SwiftFormat.md | 56 +++ Guidelines/Swift/SwiftStyle.md | 57 +++ .../Swift/SwiftUI.md | 2 + .../Testing/UnitTesting.md | 0 .../Guidelines => Guidelines}/Xcode/MCP.md | 0 .../Xcode/Security.md | 0 LICENSE | 1 + Package.resolved | 33 -- Package.swift | 44 --- README.md | 346 +++++++----------- Scripts/swift_format.sh | 63 ++++ Scripts/validate_guidelines.py | 312 ++++++++++++++++ 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 | 8 +- Templates/GlobalCodexInstructions.md | 9 + Templates/Store.swift | 112 ++++++ .../ProgressionKitTests/PKLoggingTests.swift | 100 ----- .../ProgressionKitTests.swift | 196 ---------- Tests/test_validate_guidelines.py | 128 +++++++ VERSION | 1 + 63 files changed, 1414 insertions(+), 1814 deletions(-) create mode 100644 .agents/skills/agent-guidelines-audit/SKILL.md create mode 100644 .agents/skills/agent-guidelines-audit/agents/openai.yaml delete mode 100644 .gitattributes 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/CHANGELOG.md delete mode 100644 AgentGuidelines/Guidelines/Swift/SwiftLint.md delete mode 100644 AgentGuidelines/Guidelines/Swift/SwiftStyle.md delete mode 100644 AgentGuidelines/LICENSE delete mode 100644 AgentGuidelines/README.md delete mode 100644 AgentGuidelines/Scripts/validate_guidelines.py delete mode 100644 AgentGuidelines/Tests/test_validate_guidelines.py delete mode 100644 AgentGuidelines/VERSION create mode 100644 CHANGELOG.md create mode 100644 Configurations/Swift/.editorconfig create mode 100644 Configurations/Swift/.swift-format create mode 100644 Guidelines/AgentWorkflow.md rename {AgentGuidelines/Guidelines => Guidelines}/Architecture/Redux.md (61%) rename {AgentGuidelines/Guidelines => Guidelines}/CICD.md (100%) rename {AgentGuidelines/Guidelines => Guidelines}/Development.md (76%) rename {AgentGuidelines/Guidelines => Guidelines}/Documentation.md (90%) rename {AgentGuidelines/Guidelines => Guidelines}/Git/Repositories.md (79%) rename {AgentGuidelines/Guidelines => Guidelines}/GitHub/PullRequests.md (95%) 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 (70%) create mode 100644 Guidelines/Swift/SwiftFormat.md create mode 100644 Guidelines/Swift/SwiftStyle.md rename {AgentGuidelines/Guidelines => Guidelines}/Swift/SwiftUI.md (89%) 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 create mode 100755 Scripts/swift_format.sh create mode 100644 Scripts/validate_guidelines.py 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 (83%) create mode 100644 Templates/GlobalCodexInstructions.md create mode 100644 Templates/Store.swift delete mode 100644 Tests/ProgressionKitTests/PKLoggingTests.swift delete mode 100644 Tests/ProgressionKitTests/ProgressionKitTests.swift create mode 100644 Tests/test_validate_guidelines.py create mode 100644 VERSION diff --git a/.agents/skills/agent-guidelines-audit/SKILL.md b/.agents/skills/agent-guidelines-audit/SKILL.md new file mode 100644 index 0000000..1f33702 --- /dev/null +++ b/.agents/skills/agent-guidelines-audit/SKILL.md @@ -0,0 +1,60 @@ +--- +name: agent-guidelines-audit +description: Audit completed repository work against the consumer's applicable agent-guidelines, local AGENTS.md instructions, requested scope, and declared validation workflow. Use after implementing changes and before claiming completion, handing work to the user, preparing, opening, or updating a pull request, declaring merge readiness, or preparing a release. Do not use for simple answers, read-only exploration, or work that is still actively being implemented. +--- + +# Agent Guidelines Audit + +Perform a final, evidence-based compliance pass. Treat the applicable guidelines and local instructions as the source of truth; do not duplicate their full content in this skill. + +## Establish the audit scope + +1. Re-read the user request and list every requested outcome and explicit constraint. +2. Locate the repository root and every applicable `AGENTS.md` from the current directory to that root. +3. Read the shared guides referenced by those instructions that apply to the changed files and workflow. +4. Inspect `git status`, the complete diff, and relevant untracked files. Preserve unrelated user changes. +5. Check the consumer's `AgentGuidelines/VERSION` and provenance when the task changes or depends on the synchronized subtree. Do not update it implicitly. + +## Audit the implementation + +Review the actual change rather than only checking whether files exist: + +- Confirm every requested outcome is implemented and no material behavior was dropped. +- Confirm physical folders, familiar domain grouping, filenames, declaration order, type ownership, namespacing, documentation, and `MARK` organization follow the applicable guides. Distinguish values that describe data from tools that primarily execute algorithms or accumulate behavior. +- For Redux applications, trace actions, state, reducers, middleware, services, tools, presentation models, views, and side-effect results through the complete data flow. Confirm each Redux component folder contains only that component type. +- Check that framework objects, persistence, logging, and asynchronous work remain in their allowed boundaries. +- Check SwiftUI composition, narrow inputs, local versus durable state, localization, accessibility, and safe deterministic previews where applicable. +- Check tests for the required framework, mirrored paths, shared tags, Given/When/Then structure, deterministic seams, and coverage of changed behavior and failure paths. +- Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, and noise controls when logging changed. +- Check durable documentation, package configuration, CI/CD, Xcode project configuration, security-sensitive changes, and physical-device limitations when they are in scope. Compare documented Swift and concurrency settings with the effective application and test-target settings; flag both redundant isolation annotations and missing annotations at compiler-verified boundaries. +- Search for stale type names, superseded files, direct APIs forbidden by the new architecture, empty folders, and references to removed behavior. + +## Validate the evidence + +Run the repository's declared non-destructive checks in proportion to the change: + +- formatter and strict lint; +- focused tests, followed by the declared broader test plan when warranted; +- relevant builds or package validation; +- repository-specific validators; +- `git diff --check`. + +Use fresh successful evidence already produced in the same task instead of rerunning expensive checks without reason. Distinguish automated compilation and simulator evidence from hardware, signing, deployment, or manual validation that automation cannot prove. + +## Resolve findings + +- When the user authorized implementation, fix safe in-scope findings and rerun the affected checks. +- For review-only work, report findings without modifying code. +- Do not broaden the feature, rewrite unrelated files, edit a synchronized `AgentGuidelines/` subtree, or perform commits, pushes, pull requests, merges, tags, or releases without the required authority. +- Treat an unresolved required guideline violation or missing relevant validation as a blocker to claiming completion. + +## Hand off + +Summarize: + +- the instruction and guideline areas audited; +- findings fixed during the audit; +- validation commands and outcomes; +- any deliberate deviations, unavailable evidence, or remaining blockers. + +Do not say the work is done merely because the audit ran. Say it is ready only when the requested outcome is complete and the relevant evidence passes. diff --git a/.agents/skills/agent-guidelines-audit/agents/openai.yaml b/.agents/skills/agent-guidelines-audit/agents/openai.yaml new file mode 100644 index 0000000..dc4aab7 --- /dev/null +++ b/.agents/skills/agent-guidelines-audit/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Agent Guidelines Audit" + short_description: "Audit completed work against shared guidelines" + default_prompt: "Use $agent-guidelines-audit to audit this completed change before handoff." diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 38ec4db..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. -AgentGuidelines/** linguist-generated 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 6c3d454..c6eef34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,46 +1,59 @@ -# 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. +- Keep the README guideline catalog sorted alphabetically by link label. +- 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 `📈`. -## Codex review scope +```sh +python3 Scripts/validate_guidelines.py +``` -For consumer pull requests, do not substantively review `AgentGuidelines/**` after exact tagged-tree provenance has been verified. Verify its `VERSION`, compare its tree with the matching central tag, and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request. +Fix every validation failure before releasing a version. + +## Consumer pull-request review scope + +When reviewing a consumer pull request, do not review or comment on files under `AgentGuidelines/**` after exact tagged-tree provenance has been verified. That subtree is a tracked, synchronized copy marked `linguist-generated`; substantive guideline changes are reviewed in this repository. Verify the intended `AgentGuidelines/VERSION`, compare the subtree tree with the matching central tag (for example with `git subtree split --prefix=AgentGuidelines HEAD` and a tree comparison after fetching that tag), and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request instead. + +## 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 7c39de6..0000000 --- a/AgentGuidelines/AGENTS.md +++ /dev/null @@ -1,58 +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. - -## Consumer pull-request review scope - -When reviewing a consumer pull request, do not review or comment on files under `AgentGuidelines/**` after exact tagged-tree provenance has been verified. That subtree is a tracked, synchronized copy marked `linguist-generated`; substantive guideline changes are reviewed in this repository. Verify the intended `AgentGuidelines/VERSION`, compare the subtree tree with the matching central tag (for example with `git subtree split --prefix=AgentGuidelines HEAD` and a tree comparison after fetching that tag), and verify the required `.gitattributes` rule. If provenance does not match exactly, review the subtree contents and stop the merge. Report substantive guideline feedback against the central `agent-guidelines` pull request instead. - -## 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/CHANGELOG.md b/AgentGuidelines/CHANGELOG.md deleted file mode 100644 index 995b21d..0000000 --- a/AgentGuidelines/CHANGELOG.md +++ /dev/null @@ -1,78 +0,0 @@ -# Changelog - -All notable changes to this project are documented in this file. - -## [0.0.9] - 2026-07-23 - -### Added - -- Consumer pull-request review scope that excludes synchronized `AgentGuidelines/**` files from substantive Codex and human review outside the central repository. - -## [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 - -- 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 - -- A Codex review-monitoring workflow covering paginated processing reactions and review threads, clean reviews, inline feedback, replies, thread resolution, and CI checks. - -## [0.0.2] - 2026-07-21 - -### Added - -- Standard README badge conventions for ThatFactory projects and packages. -- Git repository guidance that defaults push-capable clones to SSH remotes. -- GitHub pull-request review and merge-gate guidance. -- Updated and Revision badges to the repository README. - -### Changed - -- Updated GitHub workflows to `actions/checkout@v7` and documented using current stable action versions in new workflows. -- Clarified the Redux side-effect loop and the canonical view-projection test path. -- Expanded and tested semantic-version validation to support prerelease plus build metadata and reject invalid numeric identifiers. -- Removed the redundant README license section while retaining the MIT license badge and root license file. - -## [0.0.1] - 2026-07-21 - -### Added - -- Initial shared guidelines for Redux, Swift, SwiftUI, SwiftLint, localization, testing, documentation, package maintenance, CI/CD, Xcode MCP, and Xcode security audits. -- A consumer `AGENTS.md` template and Git subtree installation workflow. -- Structural validation for links, the documentation catalog, version metadata, subtree instructions, and public-repository safety. -- A tag-driven GitHub release workflow that validates the tag against `VERSION` and publishes changelog notes. diff --git a/AgentGuidelines/Guidelines/Swift/SwiftLint.md b/AgentGuidelines/Guidelines/Swift/SwiftLint.md deleted file mode 100644 index c37477b..0000000 --- a/AgentGuidelines/Guidelines/Swift/SwiftLint.md +++ /dev/null @@ -1,8 +0,0 @@ -# SwiftLint - -- Treat lint rules as readability and correctness tools, not as architecture. -- Fix warnings introduced by a change. -- Do not add enum namespaces, empty wrapper types, or other artificial structures solely to satisfy filename rules for pure-function files such as reducers, selectors, or middleware. -- Prefer a focused local disable with a short reason when a rule conflicts with the intended design. -- Do not disable a rule repository-wide to avoid fixing one occurrence. -- Keep the lint configuration aligned with the physical folder organization and generated-file exclusions of the consumer repository. diff --git a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md b/AgentGuidelines/Guidelines/Swift/SwiftStyle.md deleted file mode 100644 index 49a5aed..0000000 --- a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md +++ /dev/null @@ -1,25 +0,0 @@ -# Swift Style - -- Keep conditional, loop, and closure bodies on separate lines. -- Keep `guard` exits on separate lines. -- Prefer seconds-based duration APIs such as `Task.sleep(for: .seconds(10))` over nanosecond literals. -- Use `///` for documentation comments and end documentation sentences with periods. -- Use meaningful names of at least three characters. Widely established type-level conventions are allowed only when the consumer explicitly uses them. -- Keep enum cases alphabetical unless ordering communicates behavior or a local lint suppression documents the exception. -- Use `// MARK: -` to separate meaningful sections. -- Use `// MARK: - Private` when separating private implementation from non-private declarations in the same file. -- Do not add Xcode boilerplate filename, author, or creation-date headers. -- Prefer one primary type or concern per file. -- Match a type file's name to its primary type. - -Example: - -```swift -guard isEnabled else { - return -} - -withAnimation { - isPresented = true -} -``` 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 7a801a1..0000000 --- a/AgentGuidelines/README.md +++ /dev/null @@ -1,139 +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.9 \ - --squash -``` - -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: - -```gitattributes -# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. -AgentGuidelines/** linguist-generated -``` - -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.9 \ - --squash -``` - -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. - -## 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/Scripts/validate_guidelines.py b/AgentGuidelines/Scripts/validate_guidelines.py deleted file mode 100644 index b816c50..0000000 --- a/AgentGuidelines/Scripts/validate_guidelines.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the structure and public safety of the guideline repository.""" - -from __future__ import annotations - -import re -import sys -from pathlib import Path, PurePosixPath - - -ROOT = Path(__file__).resolve().parents[1] -README = ROOT / "README.md" -VERSION = ROOT / "VERSION" -CHANGELOG = ROOT / "CHANGELOG.md" - -MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") -SEMVER = re.compile( - r"^(0|[1-9][0-9]*)\." - r"(0|[1-9][0-9]*)\." - r"(0|[1-9][0-9]*)" - r"(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" - r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?" - r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$" -) -FORBIDDEN = { - "/" + "Users" + "/": "personal absolute path", - "file" + "://": "local file URL", - "mobile-ios-" + "chauffeur": "work-repository identifier", - "black" + "lane": "work-repository identifier", -} - - -def text_files() -> list[Path]: - suffixes = {".md", ".py", ".yml", ".yaml", ".txt"} - files = [path for path in ROOT.rglob("*") if path.is_file() and path.suffix in suffixes] - files.extend(path for path in (ROOT / "VERSION", ROOT / "LICENSE") if path.is_file()) - return sorted(set(files)) - - -def resolve_link(source: Path, raw_target: str) -> Path | None: - target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] - if not target or target.startswith(("#", "http://", "https://", "mailto:")): - return None - - parts = PurePosixPath(target).parts - if "AgentGuidelines" in parts: - index = parts.index("AgentGuidelines") - return ROOT.joinpath(*parts[index + 1 :]).resolve() - - return (source.parent / target).resolve() - - -def validate_links(errors: list[str]) -> None: - for source in sorted(ROOT.rglob("*.md")): - for raw_target in MARKDOWN_LINK.findall(source.read_text(encoding="utf-8")): - resolved = resolve_link(source, raw_target) - if resolved is not None and not resolved.exists(): - relative_source = source.relative_to(ROOT) - errors.append(f"{relative_source}: missing link target {raw_target!r}") - - -def validate_catalog(errors: list[str]) -> None: - readme = README.read_text(encoding="utf-8") - for guide in sorted((ROOT / "Guidelines").rglob("*.md")): - relative = guide.relative_to(ROOT).as_posix() - if f"]({relative})" not in readme: - errors.append(f"README.md: guideline is not cataloged: {relative}") - - -def validate_version(errors: list[str]) -> None: - version = VERSION.read_text(encoding="utf-8").strip() - if not SEMVER.fullmatch(version): - errors.append(f"VERSION: invalid semantic version {version!r}") - - changelog = CHANGELOG.read_text(encoding="utf-8") - if f"## [{version}]" not in changelog: - errors.append(f"CHANGELOG.md: missing release heading for {version}") - - -def validate_readme_contract(errors: list[str]) -> None: - readme = README.read_text(encoding="utf-8") - required = { - 'alt="Xcode"': "Xcode badge alt text", - "thatfactory/agent-guidelines/actions/workflows/ci.yml": "CI badge repository", - "--prefix=AgentGuidelines": "subtree destination", - "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: - errors.append(f"README.md: missing {description}: {value!r}") - - -def validate_public_content(errors: list[str]) -> None: - for path in text_files(): - contents = path.read_text(encoding="utf-8") - relative = path.relative_to(ROOT) - for forbidden, description in FORBIDDEN.items(): - if forbidden.lower() in contents.lower(): - errors.append(f"{relative}: contains {description}: {forbidden!r}") - - -def main() -> int: - errors: list[str] = [] - validate_links(errors) - validate_catalog(errors) - validate_version(errors) - validate_readme_contract(errors) - validate_public_content(errors) - - if errors: - print("Guideline validation failed:") - for error in errors: - print(f"- {error}") - return 1 - - guide_count = len(list((ROOT / "Guidelines").rglob("*.md"))) - print(f"Validated {guide_count} guidelines for version {VERSION.read_text().strip()}.") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/AgentGuidelines/Tests/test_validate_guidelines.py b/AgentGuidelines/Tests/test_validate_guidelines.py deleted file mode 100644 index c17e130..0000000 --- a/AgentGuidelines/Tests/test_validate_guidelines.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Tests for the guideline repository validator.""" - -from __future__ import annotations - -import importlib.util -import unittest -from pathlib import Path - - -VALIDATOR_PATH = Path(__file__).resolve().parents[1] / "Scripts" / "validate_guidelines.py" -SPEC = importlib.util.spec_from_file_location("validate_guidelines", VALIDATOR_PATH) -assert SPEC is not None -assert SPEC.loader is not None -VALIDATOR = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(VALIDATOR) - - -class SemanticVersionTests(unittest.TestCase): - """Verifies the supported Semantic Versioning grammar.""" - - def test_valid_versions(self) -> None: - """Accepts core, prerelease, and build metadata forms.""" - versions = ( - "0.0.2", - "1.2.3-rc.1+build.5", - "1.0.0-alpha-beta", - "1.0.0+001", - ) - - for version in versions: - with self.subTest(version=version): - self.assertIsNotNone(VALIDATOR.SEMVER.fullmatch(version)) - - def test_invalid_versions(self) -> None: - """Rejects leading zeroes and incomplete identifiers.""" - versions = ( - "01.2.3", - "1.02.3", - "1.2.03", - "1.2.3-01", - "1.2.3-rc.01", - "1.2.3+", - "1.2.3-", - ) - - for version in versions: - with self.subTest(version=version): - self.assertIsNone(VALIDATOR.SEMVER.fullmatch(version)) - - -if __name__ == "__main__": - unittest.main() diff --git a/AgentGuidelines/VERSION b/AgentGuidelines/VERSION deleted file mode 100644 index c5d54ec..0000000 --- a/AgentGuidelines/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.9 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e622148 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,149 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [0.0.16] - 2026-08-13 + +### Changed + +- Required SwiftUI dynamic properties to precede ordinary stored properties and clarified deterministic preview expectations. +- Required one top-level type per file, focused function decomposition, logical enum grouping, and consistent declaration-modifier and multiline-signature layout. +- Documented which declaration layout conventions remain review-guided because swift-format cannot enforce them without broad source reflow. + +## [0.0.15] - 2026-07-27 + +### Added + +- A shared agent-workflow guide for bounded grouping of independent repository inspections, with dependency, ordering, scope, and output-size safeguards. +- A versioned global Codex instruction template that bootstraps discovery of repository-local guidance without duplicating engineering policy. + +### Changed + +- Linked the workflow guide from the consumer template, documented the manual global Codex setup, and required alphabetical ordering of the README guideline catalog. +- Clarified that Codex review requests are automatic by default and must not be triggered manually without an explicit user request. + +## [0.0.14] - 2026-07-27 + +### Changed + +- Clarify Store/Middleware @MainActor usage. +- Removed workaround for a resolved Xcode issue. + +## [0.0.13] - 2026-07-26 + +### Added + +- A reusable `agent-guidelines-audit` skill and mandatory completion gate before handoff, pull requests, merge readiness, and releases. +- A canonical Redux Store template plus dependency-container and middleware-composition guidance. +- Consumer Stack guidance for recording toolchain, platform, strict-concurrency, and actor-isolation settings. + +### Changed + +- Clarified Redux folder ownership, familiar domain grouping, model-versus-tool classification, service-local helpers, presentation models, and one-component-per-file organization. +- Required documentation for new Swift declarations, meaningful `MARK` sections, one meaningful SwiftUI view per file, and deterministic previews where possible. +- Clarified when target isolation defaults replace explicit annotations and when compiler-verified boundaries still require them. +- Enabled conditional-import sorting and expanded validation for Swift templates, the audit skill, Stack guidance, and formatting policy. + +## [0.0.12] - 2026-07-25 + +### Added + +- Login-shell guidance for using explicitly authorized `gh` credentials exported by local shell startup configuration without exposing token values. + +## [0.0.11] - 2026-07-25 + +### Added + +- Pre-compilation Xcode build-phase guidance and a reusable `format-and-lint` command for human and agent workflows. +- An easy-to-find record of Xcode-aligned layout settings, enabled rule overrides, and deliberate non-adoptions. +- Pull-request guidance that prevents duplicate manual Codex requests when automatic review is enabled. + +### Changed + +- Enabled empty-array literals, force-try rejection, brace whitespace cleanup, `where` clauses in eligible loops, and documentation-comment validation. + +## [0.0.10] - 2026-07-24 + +### Added + +- Shared Xcode-aligned swift-format and EditorConfig configuration. +- Reusable format, warning-lint, and strict-lint commands for Swift consumers. + +### Changed + +- Replaced SwiftLint guidance with toolchain-native swift-format guidance. + +## [0.0.9] - 2026-07-23 + +### Added + +- Consumer pull-request review scope that excludes synchronized `AgentGuidelines/**` files from substantive Codex and human review outside the central repository. + +## [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 + +- 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 + +- A Codex review-monitoring workflow covering paginated processing reactions and review threads, clean reviews, inline feedback, replies, thread resolution, and CI checks. + +## [0.0.2] - 2026-07-21 + +### Added + +- Standard README badge conventions for ThatFactory projects and packages. +- Git repository guidance that defaults push-capable clones to SSH remotes. +- GitHub pull-request review and merge-gate guidance. +- Updated and Revision badges to the repository README. + +### Changed + +- Updated GitHub workflows to `actions/checkout@v7` and documented using current stable action versions in new workflows. +- Clarified the Redux side-effect loop and the canonical view-projection test path. +- Expanded and tested semantic-version validation to support prerelease plus build metadata and reject invalid numeric identifiers. +- Removed the redundant README license section while retaining the MIT license badge and root license file. + +## [0.0.1] - 2026-07-21 + +### Added + +- Initial shared guidelines for Redux, Swift, SwiftUI, SwiftLint, localization, testing, documentation, package maintenance, CI/CD, Xcode MCP, and Xcode security audits. +- A consumer `AGENTS.md` template and Git subtree installation workflow. +- Structural validation for links, the documentation catalog, version metadata, subtree instructions, and public-repository safety. +- A tag-driven GitHub release workflow that validates the tag against `VERSION` and publishes changelog notes. diff --git a/Configurations/Swift/.editorconfig b/Configurations/Swift/.editorconfig new file mode 100644 index 0000000..f3faacc --- /dev/null +++ b/Configurations/Swift/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*.swift] +indent_style = space +indent_size = 4 +tab_width = 4 +max_line_length = 120 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/Configurations/Swift/.swift-format b/Configurations/Swift/.swift-format new file mode 100644 index 0000000..ee5586f --- /dev/null +++ b/Configurations/Swift/.swift-format @@ -0,0 +1,81 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentBlankLines" : false, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "indentation" : { + "spaces" : 4 + }, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineBreakBetweenDeclarationAttributes" : false, + "lineLength" : 120, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "multilineTrailingCommaBehavior" : "keptAsWritten", + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "orderedImports" : { + "includeConditionalImports" : true, + "shouldGroupImports" : true + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "reflowMultilineStringLiterals" : "never", + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : true, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "AvoidRetroactiveConformances" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : true, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyLinesOpeningClosingBraces" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : true, + "ValidateDocumentationComments" : true + }, + "spacesAroundRangeFormationOperators" : false, + "spacesBeforeEndOfLineComments" : 2, + "tabWidth" : 4, + "version" : 1 +} diff --git a/Guidelines/AgentWorkflow.md b/Guidelines/AgentWorkflow.md new file mode 100644 index 0000000..ebf6f13 --- /dev/null +++ b/Guidelines/AgentWorkflow.md @@ -0,0 +1,49 @@ +# Agent Workflow + +Use this guide for repository investigation and tool execution. It governs how work is explored and coordinated; language, architecture, testing, and development requirements remain in their respective guides. + +This guidance is motivated by high token consumption from unnecessary model and tool cycles during read-heavy investigation, as described in [openai/codex#35050](https://github.com/openai/codex/issues/35050). It aims to avoid unnecessary cycles while preserving coverage and correctness; it does not guarantee a particular reduction in token usage. + +## Bounded investigation + +Investigate in bounded stages based on the current task. + +Within a stage, group independent, already-known read-only operations when the available tools support doing so efficiently. Examples include targeted searches, reads of already-identified files, independent metadata checks, and inspection of separate tests or call sites. + +Use an appropriate supported mechanism for grouped or concurrent execution. A current implementation might use batched tool calls, concurrent shell operations, `Promise.allSettled`, or an equivalent approach, but no particular API is required. + +Inspect every result relevant to the conclusion. Account for failed, incomplete, and contradictory results rather than treating execution as successful merely because it was grouped. + +## Dependency and ordering + +Keep operations sequential when a result determines the next step or when ordering is observable. + +This includes: + +- adaptive investigation; +- approval-sensitive operations; +- related or conflicting mutations; +- edits followed by compilation or validation; +- diagnostics whose result determines the next change; +- stateful external operations; +- waits and resumptions. + +Architecture-specific ordering requirements remain authoritative. For example, follow the Redux guide for dispatch and side-effect ordering rather than inferring that investigation-level concurrency permits runtime concurrency. + +Do not group operations merely because concurrency is available. + +## Scope and output + +Keep each stage narrowly scoped to the request. + +Prefer targeted searches, relevant line ranges, focused diagnostics, and specific log sections over broad repository, file, or log dumps. + +Bound the combined output of grouped operations so that every result can be inspected reliably. When evidence is incomplete or truncated, retrieve only the missing portion rather than repeating the full investigation. + +Do not expand the investigation merely because additional operations can be executed concurrently. + +## Efficiency + +Avoid unnecessary repeated model and tool cycles when several independent operations are already known. + +Efficiency must not reduce required coverage, bypass validation, conceal failures, or introduce unrelated work. diff --git a/AgentGuidelines/Guidelines/Architecture/Redux.md b/Guidelines/Architecture/Redux.md similarity index 61% rename from AgentGuidelines/Guidelines/Architecture/Redux.md rename to Guidelines/Architecture/Redux.md index 56ab6f3..4a28847 100644 --- a/AgentGuidelines/Guidelines/Architecture/Redux.md +++ b/Guidelines/Architecture/Redux.md @@ -11,7 +11,7 @@ Use this guide for applications that explicitly adopt the ThatFactory Redux arch - Middleware performs asynchronous work and other side effects. - Services wrap external frameworks, packages, persistence, clocks, APIs, and system capabilities. - Selectors derive shared domain information from state. -- Render-ready view state and view-only projections live beside their consuming views. +- Render-ready value models live under `Model/`; SwiftUI `View` types stay under `View/`. - Every side-effect result returns to the store as an action before it changes state. ## Data flow @@ -41,15 +41,7 @@ The store reduces the original action first, then awaits middleware and sequenti ## 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? -``` +Use one observable store as the source of truth and inject it at the application root. The canonical Store requires `Default Actor Isolation` set to `MainActor` and `nonisolated(nonsending) By Default` set to `Yes` in every application and test target that compiles or exercises it. New projects copy [the Store template](../../Templates/Store.swift) as is; do not add redundant isolation annotations or change its dispatch ordering, observation exclusions, or documentation. Dispatch is asynchronous and ordered: @@ -61,6 +53,31 @@ Dispatch is asynchronous and ordered: Use only `await store.dispatch(_:)`. Do not add a fire-and-forget dispatch API. +## Dependency composition + +Create one application-owned `DependencyContainer` that constructs and retains services, persistence, providers, and other side-effect dependencies. Create the container before the store, restore synchronous initial state through its dependencies, and pass the container to `makeMiddlewares(_:)`. + +```swift +@main +struct ExampleApp: App { + @State private var dependencies: DependencyContainer + @State private var store: AppStore + + init() { + let dependencies = DependencyContainer() + let store = AppStore( + initialState: dependencies.restoredAppState(), + middlewares: makeMiddlewares(dependencies), + reducer: appReducer + ) + _dependencies = State(initialValue: dependencies) + _store = State(initialValue: store) + } +} +``` + +Keep application bootstrap responsible for composition, not feature behavior. Do not construct individual services directly in the app after a dependency container exists. + ## Canonical physical folders These are filesystem folders, not Xcode groups. New single-application repositories use this structure by default: @@ -79,7 +96,6 @@ These are filesystem folders, not Xcode groups. New single-application repositor |-- Services/ |-- Tools/ |-- View/ -| `-- / `-- Resources/ Tests/ @@ -94,7 +110,6 @@ These are filesystem folders, not Xcode groups. New single-application repositor |-- Services/ |-- Tools/ `-- View/ - `-- / ``` A multi-target application may use a shared source root such as `Shared/Redux/` and target-specific roots such as `/View/`. Its root `AGENTS.md` must provide a concrete path map: @@ -119,7 +134,18 @@ Put application bootstrap, app delegates, scene definitions, store construction, ### Model -Put reusable domain values in `Model/`. Keep each important type in a focused file. Do not hide response models, payloads, or domain values inside action or service files merely because only one caller currently uses them. +Put domain and presentation values in `Model/`. Models describe data, state, configuration, categories, or render-ready values; their primary responsibility is not executing an algorithm or coordinating side effects. Keep each important type in a focused file. Do not hide response models, payloads, logging categories, levels, or other values inside action or service folders merely because only one caller currently uses them. + +When several models are familiar parts of one domain, group them by that domain: + +```text +Model/ +|-- Camera/ +|-- Face/ +`-- Logging/ +``` + +Use names that help a reader reason about the domain. Keep `Model/` flat while a domain has only one file; do not create a folder for every type. ### Action @@ -134,6 +160,10 @@ enum AppAction: Equatable { Name actions after what happened or what the user requested. Keep cases in the order required by the project's Swift style guide. +Declare `AppAction` and each domain action in separate files. `AppAction.swift` contains the root routing action only; do not append logging models, categories, feature actions, or unrelated supporting declarations to it. + +Every production file under `Redux/Action/` must define an action. Values carried by actions, including categories, levels, payloads, and capability descriptions, belong in `Model/`. + ### State Put the root state and domain sub-states in `Redux/State/`. Prefer focused value types with compiler-synthesized conformances. Add a new sub-state for a durable domain instead of folding unrelated values into an existing feature. @@ -142,6 +172,8 @@ State stores durable facts. Avoid storing values that are cheap, deterministic d 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. +Declare `AppState` and each domain sub-state in separate files. `AppState.swift` contains the root state only. + ### Reducer Put reducer functions in `Redux/Reducer/`. A reducer receives state and an action and returns new state. It must not: @@ -155,13 +187,17 @@ Put reducer functions in `Redux/Reducer/`. A reducer receives state and an actio Use the smallest state and action inputs that correctly express the transition. Root reducers compose domain reducers. +Declare the root reducer and each domain reducer in separate files. `AppReducer.swift` contains only root composition. Every production file under `Redux/Reducer/` must define a reducer; move events, capability values, policies, and other supporting domain types to `Model/` or their own appropriate component. + ### Middleware 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, 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: +Every production file under `Redux/Middleware/` must define or compose middleware. A helper, closure signature, or type alias used only by one middleware stays in that middleware file and should be private when its test seam and call sites allow it. Do not create a standalone middleware file for a declaration that is not middleware. + +Create a feature subfolder only when a domain has multiple middleware files: ```text Redux/Middleware/Account/ @@ -178,32 +214,33 @@ Do not put SwiftUI types, colors, images, localized display strings, or render-r ### Services -Put focused external-boundary abstractions in `Services//`. Services wrap APIs, persistence, packages, frameworks, sensors, system features, and other impure operations. Middleware calls services; views and reducers do not. +Put focused external-boundary abstractions in `Services/`. Services wrap APIs, persistence, packages, frameworks, sensors, system features, and other impure operations. Keep this folder flat while a capability has only one file; introduce a familiar capability folder such as `Services/FaceService/` or `Services/CalibrationService/` when that capability genuinely requires several related files. Middleware calls services; views and reducers do not. Prefer a protocol or otherwise injectable contract when a service must be replaced in tests. Keep transport-specific details behind the service boundary. +Keep a supporting delegate, adapter, or helper beside its service when only that capability uses it. Local ownership is clearer than promoting a service-private framework bridge to a global `Tools/` folder. + 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. +Put specialized algorithms, accumulators, framework adapters, and genuinely cross-cutting implementation utilities in `Tools/`. This is not a miscellaneous folder. A type belongs here when its primary responsibility is performing computation or implementing technical behavior rather than describing values or owning an external capability. Feature-only helpers stay beside that feature. Keep `Tools/` flat until one familiar topic requires several files, then group them under a domain folder such as `Tools/Face/`. ### View -Put SwiftUI screens and components in `View//`. A new view belongs to the feature it renders, not in Redux. Reusable visual components may use `View/Generic/` or another explicitly declared shared-view folder. +Put SwiftUI screens and components in `View/`. A new view belongs to the feature it renders, not in Redux. Reusable visual components may use `View/Generic/` or another explicitly declared shared-view folder. Keep `View/` flat while it has only a few files; introduce `View//` when a familiar feature genuinely has several views. -Render-facing view-state types and projections live beside the consuming view: +Render-facing value types that do not conform to `View` are presentation models and live under `Model//`: ```text +Model/Account/ +`-- AccountViewState.swift + View/Account/ -|-- AccountView.swift -|-- AccountViewState.swift -`-- AccountViewStateProjection.swift +`-- AccountView.swift ``` -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. +Keep a tiny private projection beside its consuming view only when it is an implementation detail rather than a named value type. ### Resources @@ -213,10 +250,12 @@ Put catalogs, assets, preview assets, configuration resources, and test plans in - Prefer one primary concern per file. - When a feature has several files of one Redux component, introduce a feature subfolder under that component. +- Group several related models, services, or tools by a familiar domain or capability so readers can reason about them together. - Keep root routing and composition at the component root; keep feature implementations below it. - File names match their primary type or clearly describe their primary pure function. - Do not introduce artificial enum namespaces solely to satisfy filename lint rules. - Mirror production organization in tests so components are easy to locate. +- Do not keep empty component folders. Add `Selector/`, `Tools/`, feature folders, or mirrored test folders only when they contain a real implementation. ## SwiftUI connection @@ -230,18 +269,18 @@ Prefer narrow view inputs or a focused view-state projection. This aligns SwiftU | Step | Change | Default destination | |---|---|---| -| 1 | Define domain models | `Model//` | +| 1 | Define domain models | `Model/` or `Model//` when several are familiar | | 2 | Define feature state | `Redux/State/State.swift` | | 3 | Add it to root state | `Redux/State/AppState.swift` | | 4 | Define feature actions | `Redux/Action/Action.swift` | | 5 | Route them through the root action | `Redux/Action/AppAction.swift` | | 6 | Implement the reducer | `Redux/Reducer/Reducer.swift` | | 7 | Compose the reducer | `Redux/Reducer/AppReducer.swift` | -| 8 | Add side effects if needed | `Redux/Middleware//` | +| 8 | Add side effects if needed | `Redux/Middleware/` or a feature folder when several | | 9 | Register middleware | `Redux/Middleware/AppMiddlewares.swift` | -| 10 | Add external boundaries if needed | `Services//` | +| 10 | Add external boundaries if needed | `Services/` or `Services//` when several | | 11 | Add shared domain selectors if needed | `Redux/Selector//` | -| 12 | Build the feature UI | `View//` | +| 12 | Build the feature UI | `View/` or `View//` when several are familiar | | 13 | Mirror tests | `Tests/` | Skip components that provide no value. A state-only transition needs no middleware; a screen-only projection does not need a Redux selector. @@ -252,7 +291,7 @@ Skip components that provide no value. A state-only transition needs no middlewa - Selector tests provide state and assert the derived domain result. - Middleware tests inject mocks, execute an action, and assert the returned follow-up action. - Service tests exercise the external boundary without involving views. -- View-state projection tests live under the matching `Tests/View//` folder, or the consumer-mapped test root. +- Presentation-model tests live under the matching `Tests/Model//` folder, or the consumer-mapped test root. - Test mocks and fixture data live under the test target's `Mocks/` folder. Follow [Unit testing](../Testing/UnitTesting.md) for framework and concurrency conventions. 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 76% rename from AgentGuidelines/Guidelines/Development.md rename to Guidelines/Development.md index afce49f..8f93d90 100644 --- a/AgentGuidelines/Guidelines/Development.md +++ b/Guidelines/Development.md @@ -19,6 +19,12 @@ 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. +## Completion audit + +Before claiming implementation is complete, handing work to the user, preparing, opening, or updating a pull request, declaring merge readiness, or preparing a release, invoke `$agent-guidelines-audit`. + +If the skill is not discoverable in a subtree consumer, read and follow its [SKILL.md](../.agents/skills/agent-guidelines-audit/SKILL.md) directly. The audit is a final verification gate, not a substitute for reading and applying the relevant guidelines during implementation. Resolve in-scope findings and rerun affected checks before handoff. Do not broaden the requested scope merely to satisfy the audit. + ## 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 2a0d327..4cc60c5 100644 --- a/AgentGuidelines/Guidelines/Documentation.md +++ b/Guidelines/Documentation.md @@ -6,7 +6,8 @@ ## Code-level documentation -- Document structs, classes, enums, protocols, actors, and other significant types with focused `///` DocC comments. +- Document every new struct, class, enum, protocol, actor, and function with focused `///` DocC comments. +- Use `// MARK: -` pragmas to separate meaningful logical sections so source files remain easy to scan and navigate. - Update documentation when changing a documented API, parameter, behavior, or invariant. - End documentation sentences with periods. - Explain intent, contracts, units, side effects, isolation, and non-obvious constraints; do not restate syntax. diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/Guidelines/Git/Repositories.md similarity index 79% rename from AgentGuidelines/Guidelines/Git/Repositories.md rename to Guidelines/Git/Repositories.md index bff7933..04e66ce 100644 --- a/AgentGuidelines/Guidelines/Git/Repositories.md +++ b/Guidelines/Git/Repositories.md @@ -40,3 +40,13 @@ When `gh` authentication appears inconsistent: 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. + +### Login-shell credentials + +Some developer environments export `GITHUB_TOKEN` from a shell startup file rather than from the non-interactive process that launched the agent. When the user has explicitly authorized using that local configuration, retry `gh` in a login shell that sources the user's startup configuration: + +```sh +zsh -lc 'source "$HOME/.zshrc"; gh auth status' +``` + +Run the required `gh` operation in that same shell after authentication succeeds. Never print, inspect, copy, or persist the token value; suppress unrelated startup output when practical, and do not source a startup file merely to bypass a credential or permission boundary without the user's authorization. diff --git a/AgentGuidelines/Guidelines/GitHub/PullRequests.md b/Guidelines/GitHub/PullRequests.md similarity index 95% rename from AgentGuidelines/Guidelines/GitHub/PullRequests.md rename to Guidelines/GitHub/PullRequests.md index 3535d36..3a2c349 100644 --- a/AgentGuidelines/Guidelines/GitHub/PullRequests.md +++ b/Guidelines/GitHub/PullRequests.md @@ -8,6 +8,7 @@ Use this guide whenever creating, reviewing, updating, or merging a GitHub pull - Follow the repository's pull-request template and local contribution instructions. - Run the relevant local validation and document anything that could not be run. - Open the pull request without auto-merge and keep it unmerged while automated or agent review is pending. Use draft state only when configured reviewers also run on drafts. +- When automatic Codex review is enabled, opening the pull request schedules the review. Do not also post `@codex review` or make another manual request; duplicate reviews waste review capacity and tokens. Do not request a Codex review manually unless the user explicitly asks for one. ## Consumer subtree review scope 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 70% rename from AgentGuidelines/Guidelines/Swift/Swift.md rename to Guidelines/Swift/Swift.md index 2f6305e..6ae523d 100644 --- a/AgentGuidelines/Guidelines/Swift/Swift.md +++ b/Guidelines/Swift/Swift.md @@ -22,9 +22,11 @@ ## State and isolation - Treat actor isolation as part of an API's contract. -- Mark UI-bound reference models `@MainActor` unless the target's default actor isolation already provides it. -- Avoid adding `@MainActor` to tests or domain types merely to silence a diagnostic. Resolve the actual isolation boundary. +- When application and test targets use MainActor default isolation, infer isolated conformances, and `nonisolated(nonsending)` by default, omit annotations that merely restate those effective settings. Verify every affected target before removing annotations. +- `nonisolated(nonsending)` by default governs how nonisolated asynchronous functions run; it does not make synchronous types or conformances nonisolated. Keep explicit `nonisolated` where a value conformance must satisfy a `Sendable` generic contract, a synchronous API is called from a `@Sendable` closure, or another compiler-verified actor boundary requires it. +- Keep an explicit isolation annotation when a declaration intentionally differs from the target default, crosses an actor boundary, belongs to reusable code compiled under different defaults, or implements a documented compiler workaround. - Use `Sendable` where values cross concurrency domains and their stored values support it. +- Avoid adding `@MainActor` to tests or domain types merely to silence a diagnostic. Resolve the actual isolation boundary. ## C-family interoperability diff --git a/Guidelines/Swift/SwiftFormat.md b/Guidelines/Swift/SwiftFormat.md new file mode 100644 index 0000000..b5060fb --- /dev/null +++ b/Guidelines/Swift/SwiftFormat.md @@ -0,0 +1,56 @@ +# Swift Format + +## Workflow + +- Treat formatting and lint rules as readability and correctness tools, not as architecture. +- Use the shared configuration under `Configurations/Swift/`; consumers expose it through root `.swift-format` and `.editorconfig` symlinks so Xcode, local commands, and CI agree. Configuration discovery is hierarchical, while an explicit `--configuration` path is unconditional. +- In Xcode, use **Editor > Structure > Format File with 'swift-format'** (or the corresponding selection command) when you want to rewrite source. +- After changing Swift source, humans and agents run `AgentGuidelines/Scripts/swift_format.sh format-and-lint ` before handoff. Do this even when a later build would provide the same safety net. +- Run `AgentGuidelines/Scripts/swift_format.sh format ` when only rewriting source is required. +- Run `AgentGuidelines/Scripts/swift_format.sh lint ` for non-blocking local warnings and `lint-strict` for errors that block CI. +- Fix findings introduced by a change. Formatter-supported rules are corrected by `format`; linter-only rules require a source change. + +## Xcode build integration + +- Add a **Swift Format** run-script phase to every independently buildable app or test target that compiles Swift source. Place it before **Compile Sources** so compilation consumes the formatted files. +- Skip the phase when `CI=true`; CI must remain non-mutating and run `lint-strict` in one dedicated job. +- Invoke `AgentGuidelines/Scripts/swift_format.sh format-and-lint` only over source folders compiled by that target, including shared folders it consumes. Exclude unrelated app and test sources so an invalid file outside the selected build cannot block compilation. +- Run the phase on every build rather than using dependency analysis. A no-op formatting pass is intentionally cheaper than allowing locally generated formatting debt. +- Source mutation requires either declared source inputs and outputs or disabling Xcode's **User Script Sandboxing** for the affected configurations. Record and review that choice locally; never disable sandboxing without the formatting phase requiring it. +- Validate the integration in Xcode with an open, deliberately misformatted file. Confirm formatting happens before compilation and that editor saving, cursor state, and undo behavior remain acceptable. + +## Shared customizations + +The checked-in configuration starts from the exhaustive Xcode toolchain dump. These deliberate overrides are the shared policy and must be reapplied when the toolchain changes. + +### Xcode-aligned layout + +- `indentation`: 4 spaces +- `tabWidth`: 4 +- `lineLength`: 120 +- `indentSwitchCaseLabels`: `false` +- Swift-only EditorConfig settings mirror indentation, line length, LF newlines, final newlines, and trailing-whitespace cleanup. + +### Rules enabled beyond the dumped defaults + +- `AlwaysUseLiteralForEmptyCollectionInit`: keeps empty arrays concise and replaces the relevant SwiftLint array/empty-collection checks. +- `NeverUseForceTry`: retains a production safety check; swift-format exempts supported test code. +- `NoEmptyLinesOpeningClosingBraces`: replaces SwiftLint's opening- and closing-brace vertical-whitespace checks. +- `UseWhereClausesInForLoops`: preserves the former SwiftLint `for_where` behavior. +- `ValidateDocumentationComments`: validates documentation already present, including parameter coverage after signature changes, without requiring every declaration to be documented. +- `includeConditionalImports`: sorts imports inside conditional-compilation blocks together with ordinary imports. + +Rules not listed here retain the exhaustive Xcode dump values. In particular, universal public documentation, force-unwrap rejection, implicit-return rewriting, early-exit rewriting, leading-underscore rejection, and implicitly unwrapped optional rejection remain disabled until adopted deliberately. swift-format has no equivalent for repository-specific import bans or sorted enum cases. + +Declaration layout rules from [Swift style](SwiftStyle.md), including keeping modifiers on the declaration line and preserving an intentionally multiline signature, remain review-guided. The formatter preserves a correctly authored layout, but it has no focused rule that forces those shapes; disabling `respectsExistingLineBreaks` would broadly reflow otherwise intentional source formatting. + +## Focused exceptions + +- Prefer a focused `// swift-format-ignore: RuleName` immediately before the affected declaration or statement when a rule conflicts with required semantics. Add a short preceding comment explaining why. +- Do not ignore a whole file or disable a shared rule to avoid fixing one occurrence. + +## Toolchain updates + +- When the supported Xcode toolchain changes, regenerate the exhaustive configuration with `xcrun swift-format dump-configuration`, reapply the documented Xcode-aligned values, review the resulting policy change, and release it centrally before consumer adoption. + +See swift-format's [configuration](https://github.com/swiftlang/swift-format/blob/main/Documentation/Configuration.md), [rule](https://github.com/swiftlang/swift-format/blob/main/Documentation/RuleDocumentation.md), and [focused suppression](https://github.com/swiftlang/swift-format/blob/main/Documentation/IgnoringSource.md) documentation for the underlying behavior. diff --git a/Guidelines/Swift/SwiftStyle.md b/Guidelines/Swift/SwiftStyle.md new file mode 100644 index 0000000..6277537 --- /dev/null +++ b/Guidelines/Swift/SwiftStyle.md @@ -0,0 +1,57 @@ +# Swift Style + +- Keep conditional, loop, and closure bodies on separate lines. +- Keep `guard` exits on separate lines. +- Prefer seconds-based duration APIs such as `Task.sleep(for: .seconds(10))` over nanosecond literals. +- Use `///` for documentation comments and end documentation sentences with periods. +- Use meaningful names of at least three characters. Widely established type-level conventions are allowed only when the consumer explicitly uses them. +- Keep enum cases alphabetical unless ordering communicates behavior or a local lint suppression documents the exception. +- Use `// MARK: -` to separate meaningful sections. +- Use `// MARK: - Private` when separating private implementation from non-private declarations in the same file. +- Break branching or multi-step implementation into small, focused functions whose names make the caller read as a sequence of intentions. Keep orchestration concise, move implementation details below `// MARK: - Private`, and avoid extracting trivial expressions that are clearer inline. +- Do not add Xcode boilerplate filename, author, or creation-date headers. +- Keep each top-level type in its own file, even when multiple types are closely related. Nest a supporting type only when it is private to one primary type and the relationship forms a natural namespace. +- Match a type file's name to its primary type. +- Put the declaration named by the file immediately after imports and file-level directives. Opening `EffectAssetLoader.swift`, for example, must reveal `EffectAssetLoader` before supporting declarations. A shared canonical template may retain type aliases that its documented layout deliberately places first. +- Keep declaration modifiers such as `nonisolated` on the same line as the declaration they modify. For a multiline function signature, keep the opening brace on the return-type line. +- Separate groups of enum cases with blank lines when the groups represent distinct operations, phases, or workflows. Keep cases consistently ordered within each group; meaningful workflow order may override alphabetical order. +- Keep physical folders flat until one topic genuinely contains several files. When grouping becomes useful, organize related models, services, tools, views, and Redux components by a familiar domain, feature, or capability so readers can reason about them together. + +Example: + +```swift +guard isEnabled else { + return +} + +withAnimation { + isPresented = true +} +``` + +Namespaced supporting types keep their ownership visible: + +```swift +struct Measurement { + // ... +} + +// MARK: - Errors + +extension Measurement { + enum ValidationError: Error { + case invalidValue + } +} +``` + +Multiline declarations keep their modifiers and braces attached to the declaration: + +```swift +nonisolated func reduce( + _ state: State, + _ action: Action +) -> State { + // ... +} +``` diff --git a/AgentGuidelines/Guidelines/Swift/SwiftUI.md b/Guidelines/Swift/SwiftUI.md similarity index 89% rename from AgentGuidelines/Guidelines/Swift/SwiftUI.md rename to Guidelines/Swift/SwiftUI.md index 4b39758..cbc3185 100644 --- a/AgentGuidelines/Guidelines/Swift/SwiftUI.md +++ b/Guidelines/Swift/SwiftUI.md @@ -4,8 +4,10 @@ Use official Apple documentation and Xcode's current SwiftUI skills for API-spec ## View structure +- Put SwiftUI dynamic properties such as `@Environment`, `@Query`, `@State`, and `@Binding` before ordinary stored `let` and `var` properties. Keep injected environment dependencies before locally owned state when both are present. - Keep a parent view focused on composition. - Model meaningful sections such as headers, lists, metadata, sidebars, and footers as separate `View` types with narrow inputs. +- Keep each independently meaningful `View` in its own file, including private supporting views. Give every view its own deterministic preview when the required dependencies can be represented safely; when they cannot, document the concrete limitation in the handoff. - Do not extract sections into computed `some View` properties merely to shorten `body`; computed properties remain in the parent's invalidation boundary. - Tiny fragments reused within one body may use a small helper when they have no independent state, input, or invalidation story. - Keep view initializers cheap. Do not decode data, access files, build large structures, or allocate formatters in `init`. 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..e29c651 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,166 @@

- 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 and development configuration. 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 and supporting configuration, not a Swift product. Consumers install a tagged release as a Git subtree at `AgentGuidelines/`, so every agent and supported tool 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 | | +| |-- Configurations/ | | +| `-- 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 + +- [Agent workflow and tool execution](Guidelines/AgentWorkflow.md) +- [CI/CD](Guidelines/CICD.md) +- [Development and reusability](Guidelines/Development.md) +- [Documentation](Guidelines/Documentation.md) +- [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) +- [GitHub pull requests](Guidelines/GitHub/PullRequests.md) +- [Localization](Guidelines/Swift/Localization.md) +- [Logging](Guidelines/Logging.md) +- [Redux architecture and physical folder organization](Guidelines/Architecture/Redux.md) +- [Swift](Guidelines/Swift/Swift.md) +- [Swift format](Guidelines/Swift/SwiftFormat.md) +- [Swift packages](Guidelines/Packages.md) +- [Swift style](Guidelines/Swift/SwiftStyle.md) +- [SwiftUI](Guidelines/Swift/SwiftUI.md) +- [Unit and integration testing](Guidelines/Testing/UnitTesting.md) +- [Xcode MCP and visual verification](Guidelines/Xcode/MCP.md) +- [Xcode security audits](Guidelines/Xcode/Security.md) + +Only reference the guides that apply. Agent workflow normally applies to both applications and packages. A UI-agnostic package normally also 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.16 \ + --squash ``` -Create an event whenever the player finishes one unit of content: +Swift consumers that adopt the shared formatter expose its configuration at the repository root so Xcode and other tools discover it: -```swift -let event = PKEvent( - contentID: "lesson.greetings.001", - trackID: "japanese-basics", - tierID: "beginner", - wasSuccessful: true -) +```sh +ln -s AgentGuidelines/Configurations/Swift/.swift-format .swift-format +ln -s AgentGuidelines/Configurations/Swift/.editorconfig .editorconfig ``` -Apply the event to the profile: +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 update = PKEngine.apply( - event: event, - to: profile -) +```gitattributes +# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. +AgentGuidelines/** linguist-generated ``` -`update` is a `PKUpdate` value that contains the updated `PKProfile` and derived progression values your app can render immediately. +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. -Common `PKUpdate` values you will typically use: +### Configure global Codex instructions -- `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. +Copy the contents of [`Templates/GlobalCodexInstructions.md`](Templates/GlobalCodexInstructions.md) into the user's global Codex instructions. -## Configure Progression Rules +These instructions only bootstrap discovery of repository-local `AGENTS.md` files and shared guides. Repository engineering policy remains versioned in this repository rather than duplicated in each user's global configuration. -Use `PKConfig` when you want to customize level size, XP rewards, tier unlock order, and the mastery requirement for unlocking the next tier: +Review this template when upgrading `agent-guidelines`, because the recommended global bootstrap instructions may change between releases. Installing or updating the Git subtree does not update a user's global Codex configuration. -```swift -let config = PKConfig( - levelXP: 120, - masteryXP: 15, - tierOrder: ["beginner", "intermediate", "advanced"], - masteryRequirement: 4 -) -``` +Redux applications also copy [the canonical Store](Templates/Store.swift) as is, following the composition and placement rules in [Redux architecture](Guidelines/Architecture/Redux.md). -Apply the same event with your custom config: +Expose the completion-audit skill at the consumer repository root so Codex can discover it: -```swift -let configuredUpdate = PKEngine.apply( - event: event, - to: profile, - config: config -) +```sh +mkdir -p .agents/skills +ln -s ../../AgentGuidelines/.agents/skills/agent-guidelines-audit \ + .agents/skills/agent-guidelines-audit ``` -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) +## Update a consumer -This example shows a simple integration pattern: apply progression events, keep the latest `PKUpdate`, and render a progress bar from the returned values. +Review the target release's changelog, then pull it deliberately: -### 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) - - GeometryReader { geometry in - let totalWidth = geometry.size.width - let fillWidth = totalWidth * progress - - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 10) - .fill(.gray.opacity(0.25)) - - RoundedRectangle(cornerRadius: 10) - .fill(.green) - .frame(width: fillWidth) - .animation(.snappy, value: progress) - } - } - .frame(height: 16) - - Text("\(Int(progress * 100))%") - .font(.caption) - .foregroundStyle(.secondary) - - Button("Complete Lesson") { - let event = PKEvent( - contentID: "lesson.greetings.\(lessonNumber)", - trackID: "japanese-basics", - tierID: "beginner", - wasSuccessful: true - ) - - let update = PKEngine.apply( - event: event, - to: profile, - config: config - ) - - withAnimation(.snappy) { - profile = update.profile - } - lessonNumber += 1 - } - } - .padding() - } -} +```sh +git subtree pull \ + --prefix=AgentGuidelines \ + https://github.com/thatfactory/agent-guidelines.git \ + 0.0.16 \ + --squash +``` -// MARK: - Preview +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. -#Preview { - ProgressionDemoView() -} -``` +## Maintain the source of truth -## Integration +1. Export current Xcode skills to a temporary review location when a new Xcode release materially changes agent behavior: -### Xcode -Use Xcode's [built-in support for SPM](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). + ```sh + xcrun agent skills export --output-dir + ``` -*or...* +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. -### Package.swift -In your `Package.swift`, add `ProgressionKit` as a dependency: +## Precedence -```swift -dependencies: [ - .package( - url: "https://github.com/thatfactory/progressionkit", - from: "0.1.4" - ) -] -``` +For a consumer task, apply instructions in this order: -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/Scripts/swift_format.sh b/Scripts/swift_format.sh new file mode 100755 index 0000000..7ed65b1 --- /dev/null +++ b/Scripts/swift_format.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + echo "Usage: $0 ..." >&2 +} + +if [[ $# -lt 2 ]]; then + usage + exit 64 +fi + +mode="$1" +shift + +script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +configuration="$script_directory/../Configurations/Swift/.swift-format" + +if command -v xcrun >/dev/null 2>&1 && xcrun --find swift-format >/dev/null 2>&1; then + formatter=(xcrun swift-format) +elif command -v swift-format >/dev/null 2>&1; then + formatter=(swift-format) +elif command -v swift >/dev/null 2>&1; then + formatter=(swift format) +else + echo "error: swift-format is unavailable; install or select a Swift 6 toolchain." >&2 + exit 127 +fi + +common_arguments=( + --configuration "$configuration" + --recursive + --parallel +) + +format_sources() { + "${formatter[@]}" format --in-place "${common_arguments[@]}" "$@" +} + +lint_sources() { + "${formatter[@]}" lint "${common_arguments[@]}" "$@" +} + +case "$mode" in + format) + format_sources "$@" + ;; + format-and-lint) + format_sources "$@" + lint_sources "$@" + ;; + lint) + lint_sources "$@" + ;; + lint-strict) + "${formatter[@]}" lint --strict "${common_arguments[@]}" "$@" + ;; + *) + usage + exit 64 + ;; +esac diff --git a/Scripts/validate_guidelines.py b/Scripts/validate_guidelines.py new file mode 100644 index 0000000..23c935d --- /dev/null +++ b/Scripts/validate_guidelines.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +"""Validate the structure and public safety of the guideline repository.""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path, PurePosixPath + + +ROOT = Path(__file__).resolve().parents[1] +README = ROOT / "README.md" +VERSION = ROOT / "VERSION" +CHANGELOG = ROOT / "CHANGELOG.md" +SWIFT_FORMAT_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".swift-format" +EDITOR_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".editorconfig" +SWIFT_FORMAT_SCRIPT = ROOT / "Scripts" / "swift_format.sh" +AUDIT_SKILL = ROOT / ".agents" / "skills" / "agent-guidelines-audit" / "SKILL.md" +DEVELOPMENT_GUIDELINE = ROOT / "Guidelines" / "Development.md" +AGENTS_TEMPLATE = ROOT / "Templates" / "AGENTS.md" +EXPECTED_SWIFT_FORMAT_RULES = { + "AllPublicDeclarationsHaveDocumentation": False, + "AlwaysUseLiteralForEmptyCollectionInit": True, + "AlwaysUseLowerCamelCase": True, + "AmbiguousTrailingClosureOverload": True, + "AvoidRetroactiveConformances": True, + "BeginDocumentationCommentWithOneLineSummary": False, + "DoNotUseSemicolons": True, + "DontRepeatTypeInStaticProperties": True, + "FileScopedDeclarationPrivacy": True, + "FullyIndirectEnum": True, + "GroupNumericLiterals": True, + "IdentifiersMustBeASCII": True, + "NeverForceUnwrap": False, + "NeverUseForceTry": True, + "NeverUseImplicitlyUnwrappedOptionals": False, + "NoAccessLevelOnExtensionDeclaration": True, + "NoAssignmentInExpressions": True, + "NoBlockComments": True, + "NoCasesWithOnlyFallthrough": True, + "NoEmptyLinesOpeningClosingBraces": True, + "NoEmptyTrailingClosureParentheses": True, + "NoLabelsInCasePatterns": True, + "NoLeadingUnderscores": False, + "NoParensAroundConditions": True, + "NoPlaygroundLiterals": True, + "NoVoidReturnOnFunctionSignature": True, + "OmitExplicitReturns": False, + "OneCasePerLine": True, + "OneVariableDeclarationPerLine": True, + "OnlyOneTrailingClosureArgument": True, + "OrderedImports": True, + "ReplaceForEachWithForLoop": True, + "ReturnVoidInsteadOfEmptyTuple": True, + "TypeNamesShouldBeCapitalized": True, + "UseEarlyExits": False, + "UseExplicitNilCheckInConditions": True, + "UseLetInEveryBoundCaseVariable": True, + "UseShorthandTypeNames": True, + "UseSingleLinePropertyGetter": True, + "UseSynthesizedInitializer": True, + "UseTripleSlashForDocumentationComments": True, + "UseWhereClausesInForLoops": True, + "ValidateDocumentationComments": True, +} + +MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") +SEMVER = re.compile( + r"^(0|[1-9][0-9]*)\." + r"(0|[1-9][0-9]*)\." + r"(0|[1-9][0-9]*)" + r"(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" + r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?" + r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$" +) +FORBIDDEN = { + "/" + "Users" + "/": "personal absolute path", + "file" + "://": "local file URL", + "mobile-ios-" + "chauffeur": "work-repository identifier", + "black" + "lane": "work-repository identifier", +} + + +def text_files() -> list[Path]: + suffixes = {".md", ".py", ".swift", ".yml", ".yaml", ".txt"} + files = [path for path in ROOT.rglob("*") if path.is_file() and path.suffix in suffixes] + files.extend(path for path in (ROOT / "VERSION", ROOT / "LICENSE") if path.is_file()) + files.extend( + path + for path in (SWIFT_FORMAT_CONFIGURATION, EDITOR_CONFIGURATION) + if path.is_file() + ) + return sorted(set(files)) + + +def resolve_link(source: Path, raw_target: str) -> Path | None: + target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] + if not target or target.startswith(("#", "http://", "https://", "mailto:")): + return None + + parts = PurePosixPath(target).parts + if "AgentGuidelines" in parts: + index = parts.index("AgentGuidelines") + return ROOT.joinpath(*parts[index + 1 :]).resolve() + + return (source.parent / target).resolve() + + +def validate_links(errors: list[str]) -> None: + for source in sorted(ROOT.rglob("*.md")): + for raw_target in MARKDOWN_LINK.findall(source.read_text(encoding="utf-8")): + resolved = resolve_link(source, raw_target) + if resolved is not None and not resolved.exists(): + relative_source = source.relative_to(ROOT) + errors.append(f"{relative_source}: missing link target {raw_target!r}") + + +def validate_catalog(errors: list[str]) -> None: + readme = README.read_text(encoding="utf-8") + for guide in sorted((ROOT / "Guidelines").rglob("*.md")): + relative = guide.relative_to(ROOT).as_posix() + if f"]({relative})" not in readme: + errors.append(f"README.md: guideline is not cataloged: {relative}") + + +def validate_version(errors: list[str]) -> None: + version = VERSION.read_text(encoding="utf-8").strip() + if not SEMVER.fullmatch(version): + errors.append(f"VERSION: invalid semantic version {version!r}") + + changelog = CHANGELOG.read_text(encoding="utf-8") + if f"## [{version}]" not in changelog: + errors.append(f"CHANGELOG.md: missing release heading for {version}") + + +def validate_readme_contract(errors: list[str]) -> None: + readme = README.read_text(encoding="utf-8") + required = { + 'alt="Xcode"': "Xcode badge alt text", + "thatfactory/agent-guidelines/actions/workflows/ci.yml": "CI badge repository", + "--prefix=AgentGuidelines": "subtree destination", + "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", + "AgentGuidelines/Configurations/Swift/.swift-format": "swift-format symlink command", + "AgentGuidelines/Configurations/Swift/.editorconfig": "EditorConfig symlink command", + ".agents/skills/agent-guidelines-audit": "completion-audit skill setup", + } + for value, description in required.items(): + if value not in readme: + errors.append(f"README.md: missing {description}: {value!r}") + + +def validate_public_content(errors: list[str]) -> None: + for path in text_files(): + contents = path.read_text(encoding="utf-8") + relative = path.relative_to(ROOT) + for forbidden, description in FORBIDDEN.items(): + if forbidden.lower() in contents.lower(): + errors.append(f"{relative}: contains {description}: {forbidden!r}") + + +def validate_swift_format_configuration(errors: list[str]) -> None: + try: + configuration = json.loads(SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + errors.append(f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: invalid JSON: {error}") + return + + expected_values = { + "indentation": {"spaces": 4}, + "indentSwitchCaseLabels": False, + "lineLength": 120, + "tabWidth": 4, + "version": 1, + } + for key, expected in expected_values.items(): + actual = configuration.get(key) + if actual != expected: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + f"{key} must be {expected!r}, found {actual!r}" + ) + + include_conditional_imports = configuration.get("orderedImports", {}).get( + "includeConditionalImports" + ) + if include_conditional_imports is not True: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + "orderedImports.includeConditionalImports must be True, " + f"found {include_conditional_imports!r}" + ) + + rules = configuration.get("rules") + if not isinstance(rules, dict) or not rules: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + "rules must be an exhaustive non-empty object" + ) + else: + missing = sorted(set(EXPECTED_SWIFT_FORMAT_RULES) - set(rules)) + unexpected = sorted(set(rules) - set(EXPECTED_SWIFT_FORMAT_RULES)) + if missing or unexpected: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + f"rule map mismatch; missing={missing!r}, unexpected={unexpected!r}" + ) + for rule in sorted(set(rules) & set(EXPECTED_SWIFT_FORMAT_RULES)): + expected = EXPECTED_SWIFT_FORMAT_RULES[rule] + actual = rules[rule] + if actual != expected: + errors.append( + f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " + f"{rule} must be {expected!r}, found {actual!r}" + ) + + +def validate_editor_configuration(errors: list[str]) -> None: + try: + contents = EDITOR_CONFIGURATION.read_text(encoding="utf-8") + except OSError as error: + errors.append( + f"{EDITOR_CONFIGURATION.relative_to(ROOT)}: cannot read configuration: {error}" + ) + return + required = { + "root = true", + "[*.swift]", + "indent_style = space", + "indent_size = 4", + "tab_width = 4", + "max_line_length = 120", + "end_of_line = lf", + "insert_final_newline = true", + "trim_trailing_whitespace = true", + } + for value in sorted(required): + if value not in contents: + errors.append( + f"{EDITOR_CONFIGURATION.relative_to(ROOT)}: missing {value!r}" + ) + + +def validate_swift_format_script(errors: list[str]) -> None: + if not SWIFT_FORMAT_SCRIPT.is_file(): + errors.append(f"{SWIFT_FORMAT_SCRIPT.relative_to(ROOT)}: missing script") + elif not os.access(SWIFT_FORMAT_SCRIPT, os.X_OK): + errors.append(f"{SWIFT_FORMAT_SCRIPT.relative_to(ROOT)}: script is not executable") + + +def validate_audit_skill(errors: list[str]) -> None: + if not AUDIT_SKILL.is_file(): + errors.append(f"{AUDIT_SKILL.relative_to(ROOT)}: missing audit skill") + return + + skill = AUDIT_SKILL.read_text(encoding="utf-8") + required_skill_values = { + "name: agent-guidelines-audit": "skill name", + "before claiming completion": "completion trigger", + "git diff --check": "diff validation", + } + for value, description in required_skill_values.items(): + if value not in skill: + errors.append( + f"{AUDIT_SKILL.relative_to(ROOT)}: missing {description}: {value!r}" + ) + + development = DEVELOPMENT_GUIDELINE.read_text(encoding="utf-8") + if "$agent-guidelines-audit" not in development: + errors.append( + f"{DEVELOPMENT_GUIDELINE.relative_to(ROOT)}: " + "missing mandatory $agent-guidelines-audit invocation" + ) + + agents_template = AGENTS_TEMPLATE.read_text(encoding="utf-8") + if "AgentGuidelines/Guidelines/Development.md" not in agents_template: + errors.append( + f"{AGENTS_TEMPLATE.relative_to(ROOT)}: missing Development.md pointer" + ) + if "## Stack" not in agents_template: + errors.append(f"{AGENTS_TEMPLATE.relative_to(ROOT)}: missing Stack section") + + +def main() -> int: + errors: list[str] = [] + validate_links(errors) + validate_catalog(errors) + validate_version(errors) + validate_readme_contract(errors) + validate_public_content(errors) + validate_swift_format_configuration(errors) + validate_editor_configuration(errors) + validate_swift_format_script(errors) + validate_audit_skill(errors) + + if errors: + print("Guideline validation failed:") + for error in errors: + print(f"- {error}") + return 1 + + guide_count = len(list((ROOT / "Guidelines").rglob("*.md"))) + print(f"Validated {guide_count} guidelines for version {VERSION.read_text().strip()}.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) 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 83% rename from AgentGuidelines/Templates/AGENTS.md rename to Templates/AGENTS.md index 4bad2bd..845abf7 100644 --- a/AgentGuidelines/Templates/AGENTS.md +++ b/Templates/AGENTS.md @@ -8,15 +8,17 @@ Describe the product or package, supported platforms, and durable constraints. L Read only the guides relevant to the task: +- [Agent workflow](AgentGuidelines/Guidelines/AgentWorkflow.md) - [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) - [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) - [SwiftUI](AgentGuidelines/Guidelines/Swift/SwiftUI.md) -- [SwiftLint](AgentGuidelines/Guidelines/Swift/SwiftLint.md) +- [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) - [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) +- [Development workflow](AgentGuidelines/Guidelines/Development.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) @@ -47,6 +49,10 @@ Replace these examples with exact repository paths: | Services | `/Services/` | | Unit tests | `Tests/` | +## Stack + +Record the supported Xcode, Swift, and platform versions. State strict-concurrency mode, default actor isolation, infer-isolated-conformance behavior, and `nonisolated(nonsending)` defaults when they apply. Clarify whether application, package, and test targets share those settings. + ## Local specialization State only rules that specialize or override the shared baseline. Explain their scope and point to local source-of-truth documentation. diff --git a/Templates/GlobalCodexInstructions.md b/Templates/GlobalCodexInstructions.md new file mode 100644 index 0000000..4949810 --- /dev/null +++ b/Templates/GlobalCodexInstructions.md @@ -0,0 +1,9 @@ +# Global Codex Instructions + +For repositories containing an `AGENTS.md`, read and follow the applicable repository instructions before starting substantive work. + +When a repository includes shared agent guidelines, read only the guides referenced by the applicable `AGENTS.md`. Treat those guides as the source of truth for language conventions, architecture, development workflow, testing, and agent execution. + +Repository and folder-level instructions may specialize the shared baseline within their scope. Do not replace deliberate repository conventions with generic global preferences. + +Do not duplicate repository guidance in global instructions. Global instructions should bootstrap discovery of the repository's own sources of truth. diff --git a/Templates/Store.swift b/Templates/Store.swift new file mode 100644 index 0000000..0120cf1 --- /dev/null +++ b/Templates/Store.swift @@ -0,0 +1,112 @@ +import Foundation +import Observation + +typealias AppStore = Store +typealias StateType = Equatable & Sendable & Codable +typealias ActionType = Equatable & Sendable +typealias Reducer = (State, Action) -> State +typealias Middleware = (State, Action) async -> Action? + +/// A class representing the state management store for the app. +/// +/// The `Store` class is responsible for managing the state of the application and handling actions +/// through a reducer and optional middlewares. It's an `@Observable`, which allows SwiftUI views +/// to observe state changes. This template requires every application and test target that compiles +/// or exercises it to set `Default Actor Isolation` to `MainActor` and +/// `nonisolated(nonsending) By Default` to `Yes`. These settings keep middleware on the main actor +/// without redundant isolation annotations. +/// +/// - Parameters: +/// - State: The type representing the state of the application. +/// Must conform to `Equatable & Sendable & Codable`. +/// - Action: The type representing actions that can be dispatched to the store. +/// Must conform to `Equatable & Sendable`. +/// +/// Example usage: +/// ``` +/// let store = AppStore(initialState: AppState(), reducer: appReducer) +/// await store.dispatch(.someAction) +/// ``` +@Observable final class Store { + private(set) var state: State + + @ObservationIgnored + private let middlewares: [Middleware] + + @ObservationIgnored + private let reducer: Reducer + + init( + initialState: State, + middlewares: [Middleware] = [], + reducer: @escaping Reducer + ) { + self.state = initialState + self.middlewares = middlewares + self.reducer = reducer + } +} + +// MARK: - Dispatcher + +extension Store { + /// Dispatches an action, awaiting the entire middleware chain before returning. + /// + /// The reducer runs first, then every middleware executes sequentially against the same + /// post-reducer state snapshot; any follow-up actions they return are dispatched + /// recursively (depth-first) and awaited too. This guarantees: + /// - Middleware executes sequentially and completes before returning. + /// - Nested actions dispatched by middleware are also awaited. + /// - State updates are fully processed before subsequent operations. + /// - Network requests don't overlap or time out due to race conditions. + /// + /// Awaiting also keeps state mutation off the synchronous SwiftUI update/layout pass, + /// avoiding the re-entrant `@Observable` mutation that crashes on iOS 26 (recursive + /// layout / `SIGTRAP`). + /// + /// For fire-and-forget dispatching from a synchronous context (e.g. a `Button` action, + /// `onAppear` / `onChange`, app startup), wrap the call in a `Task`: + /// ```swift + /// Task { await store.dispatch(action) } + /// ``` + /// When several actions must keep their relative order, dispatch them from a single `Task` + /// so they can't interleave: + /// ```swift + /// Task { + /// await store.dispatch(firstAction) + /// await store.dispatch(secondAction) + /// } + /// ``` + /// Conversely, **independent** actions are intentionally left as one `Task` per call so they + /// run concurrently — don't merge them into a single `Task` just to save lines, as that + /// serializes them (the second waits for the first's full middleware chain): + /// ```swift + /// // Independent: keep separate so neither blocks the other. + /// Task { await store.dispatch(firstAction) } + /// Task { await store.dispatch(secondAction) } + /// ``` + /// + /// - Parameter action: The action to dispatch. + func dispatch(_ action: Action) async { + state = reducer(state, action) + + // Capture the post-reducer state snapshot so all middlewares in this action's + // chain see the same state, even if nested actions mutate state during execution. + let currentState = state + + // Execute all middlewares against the same state snapshot and collect their next + // actions. This ensures every middleware for this action sees the same state (Redux pattern). + var nextActions: [Action] = [] + for middleware in middlewares { + if let nextAction = await middleware(currentState, action) { + nextActions.append(nextAction) + } + } + + // Then dispatch the collected next actions sequentially, maintaining depth-first + // execution while preserving state-snapshot consistency. + for nextAction in nextActions { + await dispatch(nextAction) + } + } +} 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/Tests/test_validate_guidelines.py b/Tests/test_validate_guidelines.py new file mode 100644 index 0000000..d250633 --- /dev/null +++ b/Tests/test_validate_guidelines.py @@ -0,0 +1,128 @@ +"""Tests for the guideline repository validator.""" + +from __future__ import annotations + +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +VALIDATOR_PATH = Path(__file__).resolve().parents[1] / "Scripts" / "validate_guidelines.py" +SPEC = importlib.util.spec_from_file_location("validate_guidelines", VALIDATOR_PATH) +assert SPEC is not None +assert SPEC.loader is not None +VALIDATOR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VALIDATOR) + + +class SemanticVersionTests(unittest.TestCase): + """Verifies the supported Semantic Versioning grammar.""" + + def test_valid_versions(self) -> None: + """Accepts core, prerelease, and build metadata forms.""" + versions = ( + "0.0.2", + "1.2.3-rc.1+build.5", + "1.0.0-alpha-beta", + "1.0.0+001", + ) + + for version in versions: + with self.subTest(version=version): + self.assertIsNotNone(VALIDATOR.SEMVER.fullmatch(version)) + + def test_invalid_versions(self) -> None: + """Rejects leading zeroes and incomplete identifiers.""" + versions = ( + "01.2.3", + "1.02.3", + "1.2.03", + "1.2.3-01", + "1.2.3-rc.01", + "1.2.3+", + "1.2.3-", + ) + + for version in versions: + with self.subTest(version=version): + self.assertIsNone(VALIDATOR.SEMVER.fullmatch(version)) + + +class SwiftFormattingConfigurationTests(unittest.TestCase): + """Verifies the shared Swift formatting contract.""" + + def test_swift_format_configuration(self) -> None: + """Accepts the exhaustive Xcode-aligned swift-format configuration.""" + errors: list[str] = [] + + VALIDATOR.validate_swift_format_configuration(errors) + + self.assertEqual(errors, []) + + def test_swift_format_configuration_rejects_undocumented_rule_change(self) -> None: + """Rejects a changed rule value even when the exhaustive key set is unchanged.""" + configuration = json.loads( + VALIDATOR.SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8") + ) + configuration["rules"]["NeverForceUnwrap"] = True + + with tempfile.TemporaryDirectory(dir=VALIDATOR.ROOT) as directory: + path = Path(directory) / ".swift-format" + path.write_text(json.dumps(configuration), encoding="utf-8") + errors: list[str] = [] + + with mock.patch.object(VALIDATOR, "SWIFT_FORMAT_CONFIGURATION", path): + VALIDATOR.validate_swift_format_configuration(errors) + + self.assertTrue( + any("NeverForceUnwrap must be False, found True" in error for error in errors) + ) + + def test_swift_format_configuration_requires_conditional_import_sorting(self) -> None: + """Rejects disabling conditional import sorting.""" + configuration = json.loads( + VALIDATOR.SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8") + ) + configuration["orderedImports"]["includeConditionalImports"] = False + + with tempfile.TemporaryDirectory(dir=VALIDATOR.ROOT) as directory: + path = Path(directory) / ".swift-format" + path.write_text(json.dumps(configuration), encoding="utf-8") + errors: list[str] = [] + + with mock.patch.object(VALIDATOR, "SWIFT_FORMAT_CONFIGURATION", path): + VALIDATOR.validate_swift_format_configuration(errors) + + self.assertTrue( + any( + "orderedImports.includeConditionalImports must be True" in error + for error in errors + ) + ) + + def test_editor_configuration(self) -> None: + """Accepts the shared Swift EditorConfig values.""" + errors: list[str] = [] + + VALIDATOR.validate_editor_configuration(errors) + + self.assertEqual(errors, []) + + +class AgentGuidelinesAuditSkillTests(unittest.TestCase): + """Verifies the mandatory completion-audit skill contract.""" + + def test_audit_skill_contract(self) -> None: + """Accepts the skill, Development rule, and consumer template.""" + errors: list[str] = [] + + VALIDATOR.validate_audit_skill(errors) + + self.assertEqual(errors, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..e3b86dd --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.16 From 8aa3a32e216465129fa70ecd1b3d0a4fb3301af2 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Thu, 13 Aug 2026 22:16:58 +0200 Subject: [PATCH 2/2] Prepare ProgressionKit 0.1.5 --- AGENTS.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6c3d454..19d9265 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Read only the guides relevant to the task: - [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) - [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) -- [SwiftLint](AgentGuidelines/Guidelines/Swift/SwiftLint.md) +- [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) - [Unit and integration testing](AgentGuidelines/Guidelines/Testing/UnitTesting.md) - [Documentation](AgentGuidelines/Guidelines/Documentation.md) - [Logging](AgentGuidelines/Guidelines/Logging.md) diff --git a/README.md b/README.md index 10f4e18..b062f81 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ In your `Package.swift`, add `ProgressionKit` as a dependency: dependencies: [ .package( url: "https://github.com/thatfactory/progressionkit", - from: "0.1.4" + from: "0.1.5" ) ] ```