From 1a25c6f984a99e88da4e1cd61cf3089a112e2ecb Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Sat, 19 Sep 2026 15:44:38 +0200 Subject: [PATCH 1/2] Adopt Agent Guidelines to 0.0.33 --- .../skills/agent-guidelines-audit/SKILL.md | 153 +++ .../agent-guidelines-audit/agents/openai.yaml | 4 + .../scripts/check_markdown_wrapping.swift | 261 +++++ .../scripts/check_xcstrings_inspection.swift | 260 +++++ AgentGuidelines/.github/workflows/ci.yml | 23 + AgentGuidelines/.github/workflows/release.yml | 46 + AgentGuidelines/.gitignore | 66 ++ AgentGuidelines/AGENTS.md | 83 ++ AgentGuidelines/CHANGELOG.md | 304 ++++++ .../Configurations/Swift/.editorconfig | 10 + .../Configurations/Swift/.swift-format | 81 ++ AgentGuidelines/Guidelines/AgentWorkflow.md | 57 ++ AgentGuidelines/Guidelines/AppStore.md | 38 + .../Guidelines/Architecture/Redux.md | 297 ++++++ AgentGuidelines/Guidelines/CICD.md | 104 ++ AgentGuidelines/Guidelines/Development.md | 56 ++ AgentGuidelines/Guidelines/Documentation.md | 78 ++ AgentGuidelines/Guidelines/Git/IgnoreFiles.md | 15 + .../Guidelines/Git/Repositories.md | 52 + .../Guidelines/GitHub/PullRequests.md | 184 ++++ AgentGuidelines/Guidelines/Localization.md | 94 ++ AgentGuidelines/Guidelines/Logging.md | 76 ++ AgentGuidelines/Guidelines/Packages.md | 173 ++++ AgentGuidelines/Guidelines/Swift/Swift.md | 38 + .../Guidelines/Swift/SwiftFormat.md | 108 +++ .../Guidelines/Swift/SwiftStyle.md | 57 ++ AgentGuidelines/Guidelines/Swift/SwiftUI.md | 63 ++ .../Guidelines/Testing/UnitTesting.md | 52 + AgentGuidelines/Guidelines/Xcode/MCP.md | 65 ++ .../Guidelines/Xcode/ProjectSettings.md | 80 ++ AgentGuidelines/Guidelines/Xcode/Security.md | 35 + AgentGuidelines/LICENSE | 22 + AgentGuidelines/README.md | 180 ++++ .../Scripts/prepare_localizable_symbols.swift | 236 +++++ AgentGuidelines/Scripts/swift_format.sh | 63 ++ .../Scripts/validate_consumer_setup.swift | 465 +++++++++ .../Scripts/validate_guidelines.swift | 830 ++++++++++++++++ .../Scripts/validate_string_catalogs.swift | 421 ++++++++ AgentGuidelines/Templates/.gitignore | 66 ++ AgentGuidelines/Templates/AGENTS.md | 121 +++ .../Templates/GlobalCodexInstructions.md | 30 + AgentGuidelines/Templates/Store.swift | 112 +++ AgentGuidelines/Tests/run_tests.swift | 911 ++++++++++++++++++ AgentGuidelines/VERSION | 1 + 44 files changed, 6471 insertions(+) create mode 100644 AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md create mode 100644 AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml create mode 100755 AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift create mode 100755 AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift create mode 100644 AgentGuidelines/.github/workflows/ci.yml create mode 100644 AgentGuidelines/.github/workflows/release.yml create mode 100644 AgentGuidelines/.gitignore create mode 100644 AgentGuidelines/AGENTS.md create mode 100644 AgentGuidelines/CHANGELOG.md create mode 100644 AgentGuidelines/Configurations/Swift/.editorconfig create mode 100644 AgentGuidelines/Configurations/Swift/.swift-format create mode 100644 AgentGuidelines/Guidelines/AgentWorkflow.md create mode 100644 AgentGuidelines/Guidelines/AppStore.md create mode 100644 AgentGuidelines/Guidelines/Architecture/Redux.md create mode 100644 AgentGuidelines/Guidelines/CICD.md create mode 100644 AgentGuidelines/Guidelines/Development.md create mode 100644 AgentGuidelines/Guidelines/Documentation.md create mode 100644 AgentGuidelines/Guidelines/Git/IgnoreFiles.md create mode 100644 AgentGuidelines/Guidelines/Git/Repositories.md create mode 100644 AgentGuidelines/Guidelines/GitHub/PullRequests.md create mode 100644 AgentGuidelines/Guidelines/Localization.md create mode 100644 AgentGuidelines/Guidelines/Logging.md create mode 100644 AgentGuidelines/Guidelines/Packages.md create mode 100644 AgentGuidelines/Guidelines/Swift/Swift.md create mode 100644 AgentGuidelines/Guidelines/Swift/SwiftFormat.md create mode 100644 AgentGuidelines/Guidelines/Swift/SwiftStyle.md create mode 100644 AgentGuidelines/Guidelines/Swift/SwiftUI.md create mode 100644 AgentGuidelines/Guidelines/Testing/UnitTesting.md create mode 100644 AgentGuidelines/Guidelines/Xcode/MCP.md create mode 100644 AgentGuidelines/Guidelines/Xcode/ProjectSettings.md create mode 100644 AgentGuidelines/Guidelines/Xcode/Security.md create mode 100644 AgentGuidelines/LICENSE create mode 100644 AgentGuidelines/README.md create mode 100755 AgentGuidelines/Scripts/prepare_localizable_symbols.swift create mode 100755 AgentGuidelines/Scripts/swift_format.sh create mode 100755 AgentGuidelines/Scripts/validate_consumer_setup.swift create mode 100755 AgentGuidelines/Scripts/validate_guidelines.swift create mode 100755 AgentGuidelines/Scripts/validate_string_catalogs.swift create mode 100644 AgentGuidelines/Templates/.gitignore create mode 100644 AgentGuidelines/Templates/AGENTS.md create mode 100644 AgentGuidelines/Templates/GlobalCodexInstructions.md create mode 100644 AgentGuidelines/Templates/Store.swift create mode 100755 AgentGuidelines/Tests/run_tests.swift create mode 100644 AgentGuidelines/VERSION diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md b/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md new file mode 100644 index 0000000..75510ae --- /dev/null +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md @@ -0,0 +1,153 @@ +--- +name: agent-guidelines-audit +description: Audit completed repository work and checked-in consumer integration against 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. +6. When the repository contains an `AgentGuidelines/` subtree, run `AgentGuidelines/Scripts/validate_consumer_setup.swift` from the consumer root. The validator detects Swift-format adoption from the root `AGENTS.md`; add `--require-swift-format` only when the repository must adopt it before that link is present. Treat failures as integration drift to fix or report before handoff. +7. Audit the root `.gitignore` against the shared [Git ignore guide](../../../Guidelines/Git/IgnoreFiles.md) and [`Templates/.gitignore`](../../../Templates/.gitignore). Compare active patterns rather than comments, blank lines, section order, or duplicates. When implementation is authorized, add every missing non-conflicting shared pattern using the template spelling, then verify its effect with `git check-ignore -v` and inspect matching tracked files with `git ls-files`; do not untrack files merely because a new rule matches them. Preserve rule order and negation semantics, and report a conflict instead of silently changing behavior. Treat an active pattern absent from the template as project-specific: accept it only when an adjacent `# Project-specific: ` comment contains a concrete non-placeholder repository-owner-approved rationale. Otherwise report the exact extra pattern and ask the repository owner to choose between updating agent-guidelines or documenting it locally; do not remove, rewrite, or silently approve it. A missing root `.gitignore`, an unresolved missing shared pattern, or an undecided undocumented extra pattern blocks audit completion. + +Do not inspect or require the user's global Codex instructions. They are user-level state outside the repository audit boundary; validate the checked-in root `AGENTS.md` contract instead. + +## 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. +- Trace every new or changed stateful, asynchronous, fallible, or lifecycle-oriented behavior and verify that its owning artifact emits privacy-safe AppLogger events for the meaningful success, failure, cancellation, recovery, and state-transition outcomes needed to diagnose it. Dependency declaration and target linkage alone do not establish logging coverage. Accept silence for pure values or utilities only when there is no meaningful event boundary and the implementation handoff records that deliberate decision. +- Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, noise controls, and focused formatter or sink tests when logging changed. +- For every Apple-platform application or Swift package in scope, except the AppLogger provider repository itself, verify integration with the shared [Logging guide](../../../Guidelines/Logging.md): confirm the AppLogger dependency is declared, the `AppLogger` library product is linked to every target that emits diagnostics, and any new project has it available in its primary runtime target before its first log call. Search the actual package or Xcode dependency graph rather than relying on an `import` alone, and treat `print`, direct `Logger` instances, or duplicate logging backends as incomplete integration when they emit project diagnostics. When implementation is authorized, add or repair the dependency and target linkage and migrate affected calls while preserving the guide's ownership, subsystem, category, emoji, privacy, severity, and noise rules; report an exact blocker when target or platform constraints make safe integration ambiguous. +- Inspect dependency manifests, resolver or lock files, Xcode package references, vendored source or binary frameworks, and equivalent dependency declarations. Compare the change with the baseline and identify every new third-party dependency or expansion of an existing third-party dependency into a new target or runtime role. Apply the shared [external dependency policy](../../../Guidelines/Development.md#external-dependencies): require explicit repository-owner approval before the dependency is introduced and require the durable exception record in repository documentation. Do not infer approval merely from an execution plan, pull-request description, implementation convenience, package popularity, or the dependency already appearing in the diff. Treat an unapproved or undocumented third-party dependency as a blocker to completion. Do not flag Apple system frameworks, the Swift standard library, ThatFactory-owned packages, or guideline-mandated tooling used only for its documented tooling role. If a newly resolved transitive third-party package will be linked into or shipped with the product, verify that its owning direct dependency is covered by an approved exception rather than dismissing it solely because it is transitive. +- Search dependency manifests, generated directories, project files, and documentation for CocoaPods or Carthage adoption. The shared [external dependency policy](../../../Guidelines/Development.md#external-dependencies) forbids both without an exception path and requires Swift Package Manager for package dependencies. When implementation is authorized, remove newly introduced adoption and its generated or configuration files; report pre-existing adoption as a completion blocker when safe migration is outside the task scope. +- Check package configuration, CI/CD, Xcode project configuration, security-sensitive changes, and physical-device limitations when they are in scope. For every Xcode project, perform the project-settings audit below. 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. +- Apply the shared [CI/CD guide](../../../Guidelines/CICD.md) to workflow and repository-automation changes. Search scripts, generated directories, workflow files, and documentation for fastlane adoption and treat it as forbidden without an exception path. Require focused ThatFactory tooling or repository-owned Swift scripts for CI/CD and delivery, and inspect new repository-owned executable scripts for compliance. For Swift-focused applications, games, and packages, accept Python or POSIX shell only when durable repository documentation identifies the missing Swift capability, exact script and task scope, runtime and dependency requirements, security and maintenance impact, validation method, and revisit or removal condition. Reject convenience, familiarity, shorter code, an existing interpreter, or another non-Swift script as justification. Treat every exception as narrow; the central `Scripts/swift_format.sh` wrapper is the retained documented exception for invoking Xcode's `swift-format` modes. +- For pull-request or merge readiness, apply the root `## Code Review Rules`: confirm the Codex review covers the current head, no allowed Codex review round is pending, every Codex review thread has a disposition, and no unresolved P0/P1 blocker remains. Treat P2/P3 observations as non-blocking and never request another Codex review unless the repository owner explicitly authorizes it. This Codex review-round budget does not apply to otherwise-authorized Reasoning Relay/ChatGPT review delegations; do not block them waiting for a Codex-budget exception. + +## Audit Xcode project settings + +For every checked-in `.xcodeproj`, read and apply the shared [Xcode project-settings guide](../../../Guidelines/Xcode/ProjectSettings.md): + +1. Identify the selected Xcode and its newest stable Swift language mode. Inspect every `PBXProject` build configuration and project-level `.xcconfig`; target-only values do not satisfy project-level ownership. +2. Require `GCC_TREAT_WARNINGS_AS_ERRORS`, `MTL_TREAT_WARNINGS_AS_ERRORS`, and `SWIFT_TREAT_WARNINGS_AS_ERRORS` to be `YES`; require `SWIFT_APPROACHABLE_CONCURRENCY = YES`, `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`, and `SWIFT_STRICT_CONCURRENCY = complete`; require `SWIFT_VERSION` to select that newest stable language mode; and require application targets to inherit `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO` in every configuration. +3. Discover every build setting exposed by the active Xcode whose name begins with `SWIFT_UPCOMING_FEATURE_`. For the selected Swift language mode, use the setting documentation and compiler diagnostics to identify which features remain opt-in. Require those settings to be `YES` at project level, and require flags for features already incorporated into the language mode to be absent so warnings-as-errors cannot turn a redundant-feature diagnostic into a build failure. Treat the settings listed in the shared guide as the Xcode 27 discovery inventory, not as flags that must all be enabled and not as a future-exhaustive list. +4. Enumerate every target and configuration, including application, unit-test, and UI-test targets, and inspect effective values with Xcode project-aware tooling or `xcodebuild -showBuildSettings`. For every application target, inspect the built `Info.plist` and confirm the `ITSAppUsesNonExemptEncryption` Boolean is `NO`. Remove redundant target copies and language-mode-redundant upcoming-feature flags. Treat a disabling or different target override for an applicable baseline setting as a violation unless an exact exception applies. +5. Before reporting a failure, search the nearest applicable `AGENTS.md` and durable project documentation linked from it for an exception naming the exact setting, scope, concrete incompatibility, replacement, impact, validation, and revisit condition. Accept and report an applicable documented exception; do not infer one from transient discussion, generic project prose, or the existing build setting itself. An application may use `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = YES` only when that exact exception documents the shipped non-exempt cryptography and resulting export-compliance workflow. +6. When implementation is authorized, move or add compliant values at project level, remove redundant target copies, and rerun build-setting inspection plus relevant builds. For review-only work, report undocumented gaps without editing. + +## Audit Swift package settings + +Enumerate every checked-in `Package.swift` that belongs to repository source, ignoring `.build` and other generated build artifacts, and treat each manifest's directory as a package root. For every package, read and apply [the shared package compiler-settings baseline](../../../Guidelines/Packages.md#compiler-settings-baseline); do not infer package rules solely from the Xcode guide. + +1. Identify the selected Swift/Xcode toolchain, inspect `// swift-tools-version:` and `swiftLanguageModes`, and require the newest stable language mode from the shared baseline, currently Swift 6. Reject an older effective mode unless an exact documented exception applies. +2. Run `swift package --package-path dump-package`, or the selected-toolchain equivalent, and use the evaluated manifest to enumerate locally defined targets and their effective settings. Do not rely only on textual grep because manifests may construct or mutate settings programmatically. +3. For every locally defined target that compiles Swift and for which SwiftPM exposes `swiftSettings`, including production, test, and other applicable Swift target kinds, require unconditional `.treatAllWarnings(as: .error)` plus `ExistentialAny`, `InferIsolatedConformances`, `InternalImportsByDefault`, `MemberImportVisibility`, and `NonisolatedNonsendingByDefault` as `.enableUpcomingFeature(...)` settings. Do not require Swift settings on binary targets, system-library targets, or package plug-in targets because `Target.plugin(...)` does not expose `swiftSettings`. +4. For locally compiled C or Objective-C targets, require the applicable typed `CSetting.treatAllWarnings(as: .error)`; require the corresponding `CXXSetting` where C++ is compiled. Do not require C-family settings when those languages are absent, and do not accept `unsafeFlags` in place of available typed APIs. +5. In Swift 6 mode, treat complete strict concurrency as supplied by the language mode and require every redundant explicit `StrictConcurrency` opt-in to be removed, including `.enableUpcomingFeature("StrictConcurrency")`, `.enableExperimentalFeature("StrictConcurrency")`, and `StrictConcurrency=complete` representations found in the evaluated manifest or effective compiler arguments. Require other upcoming-feature declarations to be removed when they become unconditional in the selected language mode, especially because warnings-as-errors can promote the resulting diagnostics. +6. Do not require or automatically add `.defaultIsolation(MainActor.self)`. Its absence is compliant with the shared package baseline; source-level actor-isolation review remains a separate Swift and concurrency concern. +7. Search the nearest applicable `AGENTS.md` and linked durable documentation before reporting a package-setting failure. Accept only an exception that names the exact setting or feature, package and targets, incompatibility, replacement or omission, impact, compensating validation where applicable, and revisit condition. Do not infer an exception from the manifest. +8. Run the package's documented validation workflow and, at minimum where applicable, `swift build` and `swift test`. Warnings promoted to errors must leave both warning-clean. When conditions or helper logic make the effective invocation uncertain, use verbose SwiftPM output to verify `-warnings-as-errors`, the language mode, and every `-enable-upcoming-feature` argument. + +For authorized implementation work, repair the manifest and rerun evaluated-manifest inspection plus builds. For review-only work, report the exact missing, conditional, redundant, or conflicting setting without modifying the package. + +## Audit localization + +When the repository contains localized targets or String Catalogs, read the shared [Localization guide](../../../Guidelines/Localization.md) and the consumer's local translation guidance: + +1. Keep supported languages, catalog and source paths, product voice, glossary, non-translatable terms, and project-specific exceptions in consumer documentation. Do not move those specifics into shared guidance or infer them from another product. +2. Confirm the project uses generated localizable symbols for maintained Swift catalog entries, does not check generated Swift into source control, and does not add localizable Swift literals that bypass the generated API. +3. Require the synchronized `prepare_localizable_symbols.swift` and `validate_string_catalogs.swift` logic. A local wrapper may supply project paths and languages to preserve a stable developer or CI command, but it must not retain a forked copy of shared migration or validation logic. +4. Run the consumer's documented nonmutating preparation check and catalog validator. Confirm CI runs the validator for localized projects and that every configured catalog and Swift source root is covered. +5. Inspect stale entries, required-language coverage, translation states, plural variants, and format placeholders in context. Require source/translated-language, long-text, plural, and right-to-left verification when affected. +6. Establish the explicit Git base for the completed change. If any added, copied, modified, renamed, or untracked `.xcstrings` file exists relative to that base, open every changed catalog in Xcode and inspect its String Catalog editor diagnostics. Record the repository-relative catalog path, selected Xcode version and build, and an explicit result of zero editor errors and zero editor warnings for each catalog. +7. From the consumer root, run `AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift --base-ref --inspected-catalog --evidence-output `, repeating `--inspected-catalog` for every changed catalog. In this source repository, use `.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift`. Keep the JSON evidence outside the repository and summarize its catalog paths, Xcode build, and zero-diagnostic results in the completion handoff and pull-request description. +8. Fail closed when a changed catalog lacks that recorded editor evidence. A catalog validator, `xcstringstool`, warning-clean build, test run, or unrecorded statement that Xcode was checked is not a substitute. Do not claim audit success, open or update a pull request, or declare merge readiness until the evidence gate passes. +9. Preserve machine-translation state until fluent review. Treat missing required validation, unresolved catalog errors or warnings, missing catalog-editor evidence, or undocumented project-specific deviations as incomplete implementation. + +## Audit App Store metadata + +When a repository contains an `AppStore/` directory or the change affects storefront content, read the shared [App Store metadata guide](../../../Guidelines/AppStore.md) and the consumer's product-specific App Store documentation: + +1. Confirm the repository remains the source of truth and follows the app-store-connect-mcp directory format. Keep the app identity, platform versions, locales, product copy, screenshot production, reviewer prerequisites, and export-compliance rationale in consumer documentation. +2. Verify that changed fields and files match the requested domains and locales. Treat omitted fields as unmanaged, preserve curated content outside scope, and apply the consumer's localization voice and terminology to storefront copy. +3. Require scope-matched `validate_repository` and immutable plan evidence for any requested remote synchronization. Inspect additions, changes, removals, destructive modes, operation IDs, plan identity, and digest before application. +4. After an authorized apply, inspect uncertain outcomes through `get_operation_status` without replaying writes, then plan the unchanged scope again and require `noOp: true` remote reconciliation. +5. Confirm `.appstore-connect-mcp/` is ignored and that repository content, diffs, logs, and evidence contain no credentials, private contacts, demo secrets, upload URLs, or operational journals. +6. Treat App Store submission and release as separately authorized operations. Ordinary metadata or asset synchronization never proves release readiness and never authorizes submission. + +## Audit documentation consistency + +When implementation, configuration, or workflow behavior changed, perform an explicit documentation-drift pass: + +1. Read the applicable [Documentation guide](../../../Guidelines/Documentation.md) and identify code-level or durable project documentation that describes the affected feature, API, configuration, workflow, or invariant. +2. Compare those documented claims with the final implementation. Require a documentation update when the change alters durable or core behavior, or when any existing documented claim becomes inaccurate, incomplete, misleading, or obsolete, regardless of change size. +3. Search relevant durable documentation for changed names, removed behavior, defaults, examples, diagrams, setup steps, and references. Inspect matches in context rather than assuming a keyword search alone proves consistency. +4. Do not require new project-level prose for incidental implementation details that are not durable and do not affect an existing documented claim. +5. When implementation is authorized, update or remove stale documentation in the same change. For review-only work, report the drift without editing. Known stale documentation blocks completion. + +## Audit documentation formatting + +Apply the conventions in the applicable [Documentation guide](../../../Guidelines/Documentation.md) to governed Markdown: + +1. Audit every added or changed Markdown file outside a synchronized, provenance-verified `AgentGuidelines/` subtree. +2. When the completed change adds or changes a documentation convention, or updates a consumer to a guideline release that does so, also audit the consumer's existing root Markdown files and declared durable documentation folders. This adoption pass is required even when those files did not otherwise change. +3. From the repository root, run `.agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift ` against those files or folders. The checker is read-only and reports prose paragraphs, list items, ordinary blockquotes, and GitHub alert body paragraphs that span multiple physical lines while excluding alert marker lines, fenced code, and other common verbatim Markdown constructs. +4. Inspect each reported span in context. Join confirmed hard-wrapped prose so each paragraph, list item, or blockquote occupies one physical line. Preserve intentional structure such as headings, separate list items, tables, fenced code, and ASCII diagrams. +5. When implementation is authorized, fix confirmed violations and rerun the checker. For review-only work, report them without editing. Do not claim the audit passes while a confirmed line-wrapping violation remains in scope. + +## 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`. + +When the shared Swift-format guide applies: + +- For implementation work, run `AgentGuidelines/Scripts/swift_format.sh format-and-lint` over every changed or applicable checked-in Swift source root before tests. For review-only work, use `lint-strict` so the audit does not mutate files. +- Confirm the root `.swift-format` and `.editorconfig` symlinks resolve to the synchronized shared configurations. +- Confirm pull-request and protected-branch CI run the shared wrapper with `lint-strict` in a dedicated non-mutating job. Reject `format` or `format-and-lint` in CI and verify the listed paths cover the repository's checked-in Swift roots. +- For Xcode projects, verify every independently buildable app or test target has the target-scoped pre-compilation phase described by the guide, including its `CI=true` bypass. +- For Swift packages, format `Package.swift`, `Sources`, `Tests`, and other checked-in Swift roots that exist before running `swift test`. Do not require `swift build` or `swift test` themselves to rewrite source; formatting and testing are consecutive, independently visible checks. + +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; +- consumer-integration validation and any drift found; +- `.gitignore` reconciliation, including added shared patterns and every accepted or unresolved project-specific entry; +- findings fixed during the audit; +- documentation updated or removed, or why no documentation change was required; +- validation commands and outcomes; +- changed-String-Catalog detection and, when applicable, the recorded Xcode version, catalog paths, and zero catalog-editor errors and warnings; +- 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/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml b/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml new file mode 100644 index 0000000..f7ecc03 --- /dev/null +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Agent Guidelines Audit" + short_description: "Audit work and shared-guideline integration" + default_prompt: "Use $agent-guidelines-audit to audit this completed change and its consumer guideline integration before handoff." diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift b/AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift new file mode 100755 index 0000000..703d581 --- /dev/null +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift @@ -0,0 +1,261 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +/// A prose block that should occupy one physical line. +struct Finding { + let path: String + let start: Int + let end: Int + let kind: String +} + +/// A candidate prose block being assembled. +struct Block { + let kind: String + let start: Int + var end: Int +} + +let fencePattern = #"^\s*(`{3,}|~{3,})"# +let headingPattern = #"^\s{0,3}#{1,6}(?:\s|$)"# +let listItemPattern = #"^(\s{0,3})(?:[-+*]|\d+[.)])\s+(.*)$"# +let linkDefinitionPattern = #"^\s{0,3}\[[^]]+\]:\s*\S+"# +let thematicBreakPattern = #"^\s{0,3}(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$"# +let setextUnderlinePattern = #"^\s{0,3}(?:=+|-+)\s*$"# +let tableDelimiterPattern = #"^\s*\|?(?:\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?\s*$"# +let quotePattern = #"^\s{0,3}>\s?"# +let alertMarkerPattern = #"^\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$"# + +/// Returns the first regular-expression match in a string. +func firstMatch(_ pattern: String, in value: String) -> NSTextCheckingResult? { + let expression = try? NSRegularExpression(pattern: pattern) + let range = NSRange(value.startIndex.. String? { + let range = match.range(at: index) + guard range.location != NSNotFound, let swiftRange = Range(range, in: value) else { + return nil + } + return String(value[swiftRange]) +} + +/// Returns whether a line has the shape of a Markdown table row. +func isTableRow(_ line: String) -> Bool { + let stripped = line.trimmingCharacters(in: .whitespacesAndNewlines) + return firstMatch(tableDelimiterPattern, in: stripped) != nil + || (stripped.hasPrefix("|") && stripped.hasSuffix("|")) +} + +/// Returns whether a line should terminate prose-block detection. +func isVerbatimOrStructure(_ line: String) -> Bool { + let stripped = line.trimmingCharacters(in: .whitespacesAndNewlines) + return stripped.isEmpty + || firstMatch(headingPattern, in: line) != nil + || firstMatch(thematicBreakPattern, in: line) != nil + || firstMatch(setextUnderlinePattern, in: line) != nil + || firstMatch(linkDefinitionPattern, in: line) != nil + || firstMatch(listItemPattern, in: line) != nil + || isTableRow(line) + || line.hasPrefix(" ") + || stripped.hasPrefix("<") +} + +/// Expands file and directory arguments into unique Markdown files. +func markdownFiles(_ paths: [String]) throws -> [String] { + let fileManager = FileManager.default + var files = Set() + for path in paths { + var isDirectory: ObjCBool = false + guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) else { + throw NSError( + domain: "MarkdownWrapping", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "path does not exist: \(path)" + ]) + } + if isDirectory.boolValue { + guard let enumerator = fileManager.enumerator(atPath: path) else { + continue + } + for case let candidate as String in enumerator where candidate.lowercased().hasSuffix(".md") { + let fullPath = URL(fileURLWithPath: path).appendingPathComponent(candidate).path + var candidateIsDirectory: ObjCBool = false + if fileManager.fileExists(atPath: fullPath, isDirectory: &candidateIsDirectory), + !candidateIsDirectory.boolValue + { + files.insert(fullPath) + } + } + } else if URL(fileURLWithPath: path).pathExtension.lowercased() == "md" { + files.insert(path) + } + } + return files.sorted() +} + +/// Finds prose blocks that span multiple physical lines in one Markdown file. +func findings(for path: String) throws -> [Finding] { + let contents = try String(contentsOfFile: path, encoding: .utf8) + let lines = contents.components(separatedBy: .newlines) + var findings: [Finding] = [] + var block: Block? + var fenceMarker: Character? + var inComment = false + var inFrontmatter = lines.first?.trimmingCharacters(in: .whitespacesAndNewlines) == "---" + var previousQuoteDepth = 0 + + func finishBlock() { + if let block, block.end > block.start { + findings.append(Finding(path: path, start: block.start, end: block.end, kind: block.kind)) + } + block = nil + } + + for (offset, line) in lines.enumerated() { + let lineNumber = offset + 1 + let stripped = line.trimmingCharacters(in: .whitespacesAndNewlines) + var quoteDepth = 0 + var quoteContent = line + while let match = firstMatch(quotePattern, in: quoteContent), + let range = Range(match.range, in: quoteContent) + { + quoteContent.removeSubrange(range) + quoteDepth += 1 + } + defer { + previousQuoteDepth = quoteDepth + } + + if inFrontmatter { + if lineNumber > 1, stripped == "---" { + inFrontmatter = false + } + continue + } + + if let match = firstMatch(fencePattern, in: line), let marker = capture(1, from: match, in: line)?.first { + if fenceMarker == nil { + finishBlock() + fenceMarker = marker + } else if fenceMarker == marker { + fenceMarker = nil + } + continue + } + if fenceMarker != nil { + continue + } + + if inComment { + if line.contains("-->") { + inComment = false + } + continue + } + if line.contains("") { + inComment = true + } + continue + } + + if quoteDepth > 0 { + let trimmedQuoteContent = quoteContent.trimmingCharacters(in: .whitespacesAndNewlines) + if quoteDepth == 1, previousQuoteDepth == 0, + firstMatch(alertMarkerPattern, in: trimmedQuoteContent) != nil + { + finishBlock() + continue + } + if isVerbatimOrStructure(quoteContent) { + finishBlock() + continue + } + let kind = "block quote (depth \(quoteDepth))" + if block?.kind == kind { + block?.end = lineNumber + } else { + finishBlock() + block = Block(kind: kind, start: lineNumber, end: lineNumber) + } + continue + } + + if let match = firstMatch(listItemPattern, in: line) { + finishBlock() + let content = capture(2, from: match, in: line) ?? "" + if !content.isEmpty, !isVerbatimOrStructure(content) { + block = Block(kind: "list item", start: lineNumber, end: lineNumber) + } + continue + } + + if block?.kind == "list item", line.hasPrefix(" ") || line.hasPrefix("\t") { + if isVerbatimOrStructure(line.trimmingCharacters(in: .whitespaces)) { + finishBlock() + } else { + block?.end = lineNumber + } + continue + } + + if isVerbatimOrStructure(line) { + finishBlock() + continue + } + + if block?.kind == "paragraph" { + block?.end = lineNumber + } else { + finishBlock() + block = Block(kind: "paragraph", start: lineNumber, end: lineNumber) + } + } + finishBlock() + return findings +} + +/// Writes text to standard error. +func writeError(_ value: String) { + FileHandle.standardError.write(Data((value + "\n").utf8)) +} + +/// Runs the wrapping audit and returns a lint-style status code. +func main() -> Int32 { + let paths = Array(CommandLine.arguments.dropFirst()) + if paths.isEmpty || paths.contains("--help") { + print("Usage: check_markdown_wrapping.swift ") + return paths.isEmpty ? 2 : 0 + } + do { + let files = try markdownFiles(paths) + let allFindings = try files.flatMap { try findings(for: $0) } + for finding in allFindings { + print( + "\(finding.path):\(finding.start): \(finding.kind) spans physical lines " + + "\(finding.start)-\(finding.end); keep its prose on one line" + ) + } + if !allFindings.isEmpty { + writeError("Found \(allFindings.count) hard-wrapped Markdown prose block(s).") + return 1 + } + print("Checked \(files.count) Markdown file(s); no hard-wrapped prose found.") + return 0 + } catch { + writeError("Markdown wrapping audit failed: \(error.localizedDescription)") + return 2 + } +} + +exit(main()) diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift b/AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift new file mode 100755 index 0000000..1478467 --- /dev/null +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift @@ -0,0 +1,260 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +/// Parsed command-line values for String Catalog inspection evidence. +struct Arguments { + var repository = FileManager.default.currentDirectoryPath + var baseRef: String? + var inspectedCatalogs: [String] = [] + var evidenceOutput: String? +} + +/// Runs a process in a repository and returns its standard output. +func run(_ command: [String], repositoryRoot: String) throws -> Data { + let process = Process() + let output = Pipe() + process.currentDirectoryURL = URL(fileURLWithPath: repositoryRoot) + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = command + process.standardOutput = output + process.standardError = output + try process.run() + let outputData = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + guard process.terminationStatus == 0 else { + let detail = + String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + ?? "command failed" + throw NSError( + domain: "StringCatalogInspection", code: Int(process.terminationStatus), + userInfo: [ + NSLocalizedDescriptionKey: "\(command.joined(separator: " ")): \(detail)" + ]) + } + return outputData +} + +/// Returns the Git repository root containing a requested path. +func repositoryRoot(containing repository: String) throws -> String { + let data = try run(["git", "rev-parse", "--show-toplevel"], repositoryRoot: repository) + guard let root = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), + !root.isEmpty + else { + throw NSError( + domain: "StringCatalogInspection", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "git returned no repository root" + ]) + } + return URL(fileURLWithPath: root).standardizedFileURL.path +} + +/// Parses NUL-separated Git paths and retains String Catalogs. +func catalogPaths(from data: Data) -> Set { + guard let output = String(data: data, encoding: .utf8) else { + return [] + } + return Set(output.split(separator: "\0").map(String.init).filter { $0.hasSuffix(".xcstrings") }) +} + +/// Returns added, copied, modified, renamed, and untracked String Catalogs. +func changedCatalogs(root: String, baseRef: String) throws -> Set { + _ = try run(["git", "rev-parse", "--verify", "\(baseRef)^{commit}"], repositoryRoot: root) + let tracked = try run( + ["git", "diff", "--name-only", "-z", "--diff-filter=ACMR", baseRef, "--"], + repositoryRoot: root + ) + let untracked = try run( + ["git", "ls-files", "--others", "--exclude-standard", "-z"], + repositoryRoot: root + ) + return catalogPaths(from: tracked).union(catalogPaths(from: untracked)) +} + +/// Normalizes repository-relative catalog paths supplied as inspection evidence. +func normalizedInspections(_ values: [String]) throws -> Set { + var inspections = Set() + for value in values { + let components = NSString(string: value).pathComponents + guard !NSString(string: value).isAbsolutePath, + !components.contains(".."), + value.hasSuffix(".xcstrings") + else { + throw NSError( + domain: "StringCatalogInspection", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: + "--inspected-catalog values must be repository-relative .xcstrings paths" + ]) + } + inspections.insert(NSString(string: value).standardizingPath) + } + return inspections +} + +/// Returns fail-closed coverage errors for catalog-editor inspection evidence. +func inspectionCoverageErrors(changed: Set, inspected: Set) -> [String] { + let missing = changed.subtracting(inspected).sorted().map { + "missing Xcode catalog-editor inspection evidence: \($0)" + } + let unexpected = inspected.subtracting(changed).sorted().map { + "inspection evidence does not match a changed String Catalog: \($0)" + } + return missing + unexpected +} + +/// Returns the selected Xcode version and build used for the inspection record. +func selectedXcodeVersion(root: String) throws -> String { + let data = try run(["xcodebuild", "-version"], repositoryRoot: root) + return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" +} + +/// Writes structured, non-repository evidence for a completed editor inspection. +func writeEvidence( + output: String, + root: String, + baseRef: String, + xcodeVersion: String, + catalogs: Set +) throws { + let rootURL = URL(fileURLWithPath: root).standardizedFileURL.resolvingSymlinksInPath() + let outputURL = URL(fileURLWithPath: output).standardizedFileURL.resolvingSymlinksInPath() + let rootPath = rootURL.path.hasSuffix("/") ? rootURL.path : rootURL.path + "/" + guard outputURL.path != rootURL.path, !outputURL.path.hasPrefix(rootPath) else { + throw NSError( + domain: "StringCatalogInspection", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "--evidence-output must be outside the repository" + ]) + } + try FileManager.default.createDirectory( + at: outputURL.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let evidence: [String: Any] = [ + "schemaVersion": 1, + "recordedAt": formatter.string(from: Date()), + "baseRef": baseRef, + "xcodeVersion": xcodeVersion, + "catalogs": catalogs.sorted().map { + [ + "path": $0, + "catalogEditorErrors": 0, + "catalogEditorWarnings": 0, + ] + }, + ] + let data = try JSONSerialization.data(withJSONObject: evidence, options: [.prettyPrinted, .sortedKeys]) + var contents = data + contents.append(0x0A) + try contents.write(to: outputURL, options: .atomic) +} + +/// Parses command-line arguments. +func parseArguments(_ values: [String]) throws -> Arguments { + var arguments = Arguments() + var index = 0 + while index < values.count { + let value = values[index] + if value == "--help" { + print( + "Usage: check_xcstrings_inspection.swift --base-ref " + + "[--repository ] [--inspected-catalog ...] " + + "[--evidence-output ]" + ) + exit(0) + } + guard index + 1 < values.count else { + throw NSError( + domain: "StringCatalogInspection", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "missing value for \(value)" + ]) + } + let next = values[index + 1] + switch value { + case "--repository": arguments.repository = next + case "--base-ref": arguments.baseRef = next + case "--inspected-catalog": arguments.inspectedCatalogs.append(next) + case "--evidence-output": arguments.evidenceOutput = next + default: + throw NSError( + domain: "StringCatalogInspection", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "unknown argument: \(value)" + ]) + } + index += 2 + } + guard arguments.baseRef != nil else { + throw NSError( + domain: "StringCatalogInspection", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "--base-ref is required" + ]) + } + return arguments +} + +/// Writes text to standard error. +func writeError(_ value: String) { + FileHandle.standardError.write(Data((value + "\n").utf8)) +} + +/// Validates inspection coverage and writes the structured evidence record. +func main() -> Int32 { + do { + let arguments = try parseArguments(Array(CommandLine.arguments.dropFirst())) + let root = try repositoryRoot(containing: arguments.repository) + guard let baseRef = arguments.baseRef else { + return 2 + } + let changed = try changedCatalogs(root: root, baseRef: baseRef) + let inspected = try normalizedInspections(arguments.inspectedCatalogs) + var errors = inspectionCoverageErrors(changed: changed, inspected: inspected) + if !changed.isEmpty, arguments.evidenceOutput == nil { + errors.append("--evidence-output is required when String Catalogs changed") + } + if !errors.isEmpty { + for error in errors { + writeError("String Catalog inspection audit failed: \(error)") + } + return 1 + } + if changed.isEmpty { + print( + "No changed String Catalogs relative to \(baseRef); " + + "Xcode catalog-editor inspection evidence is not required." + ) + return 0 + } + guard let evidenceOutput = arguments.evidenceOutput else { + return 1 + } + try writeEvidence( + output: evidenceOutput, + root: root, + baseRef: baseRef, + xcodeVersion: try selectedXcodeVersion(root: root), + catalogs: changed + ) + print( + "Recorded zero Xcode catalog-editor errors and warnings for " + + "\(changed.count) changed String Catalog(s) at \(evidenceOutput)." + ) + return 0 + } catch { + writeError("String Catalog inspection audit failed: \(error.localizedDescription)") + return 2 + } +} + +exit(main()) diff --git a/AgentGuidelines/.github/workflows/ci.yml b/AgentGuidelines/.github/workflows/ci.yml new file mode 100644 index 0000000..af5e61e --- /dev/null +++ b/AgentGuidelines/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + validate: + name: Validate guidelines + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Validate + run: | + Tests/run_tests.swift + Scripts/validate_guidelines.swift diff --git a/AgentGuidelines/.github/workflows/release.yml b/AgentGuidelines/.github/workflows/release.yml new file mode 100644 index 0000000..6e27cca --- /dev/null +++ b/AgentGuidelines/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: Release + +on: + push: + tags: + - "*.*.*" + +permissions: + contents: write + +jobs: + release: + name: Create GitHub release + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Validate guidelines + run: | + Tests/run_tests.swift + Scripts/validate_guidelines.swift + + - 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 new file mode 100644 index 0000000..3a3fc11 --- /dev/null +++ b/AgentGuidelines/.gitignore @@ -0,0 +1,66 @@ +# macOS +.DS_Store + +# Xcode user data, build output, and archives +xcuserdata/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.hmap +/build/ +/DerivedData/ +*.ipa +*.dSYM +*.dSYM.zip + +# Xcode playground generated state +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +/.build/ +/Packages/ +.swiftpm/configuration/registries.json + +# Node.js +node_modules/ +dist/ +coverage/ +*.tgz +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Local credentials and environment configuration +.env +.env.* +!.env.example +!.env.*.example +.netrc + +# ThatFactory tooling state +.appstore-connect-mcp/ +.devspace/ +/public-check/ + +# Project-specific entries require a durable rationale after repository-owner review. +# Project-specific: diff --git a/AgentGuidelines/AGENTS.md b/AgentGuidelines/AGENTS.md new file mode 100644 index 0000000..36e9089 --- /dev/null +++ b/AgentGuidelines/AGENTS.md @@ -0,0 +1,83 @@ +# 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. +- 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: + + ```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 + ``` + +## Repository scripts + +- Write new repository-owned executable scripts in Swift using the standard library and Foundation. +- `Scripts/swift_format.sh` is the sole retained shell-script exception because it is the existing command wrapper around Xcode's `swift-format`; do not use it as precedent for new shell automation. +- Do not add Python, Ruby, JavaScript, or other scripting-language runtimes for repository automation. + + +## Code Review Rules + +Review for release-blocking defects introduced or materially exposed by the pull request. A clean review means no unresolved P0/P1 findings; it does not mean exhaustive or perfect software. + +A blocking finding must identify a concrete, reachable path in a supported use case or the documented threat model that can cause a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. + +For every blocking finding, state the severity, preconditions, execution path, impact, evidence, and actionable remediation. Group manifestations that share the same root cause into one finding. + +Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, behavior by components outside the threat model, style preferences, and speculative refactoring. Record a useful lower-severity observation once as deferred, declined, duplicate, or follow-up work; do not keep the review loop open for it. + +In an initial review, report substantiated blockers together. A follow-up review is limited to unresolved P0/P1 findings, changes since the last reviewed commit, and code directly affected by those changes. Do not restart an unrestricted review of unchanged code. A new follow-up finding must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +The review-round budget below applies only to Codex GitHub reviews: the configured automatic Codex review and any manual `@codex review` request. It does not apply to ChatGPT review or reasoning delegated through Reasoning Relay. An otherwise-authorized Reasoning Relay workflow may request as many Relay review or follow-up delegations as its own governing workflow requires; those requests neither consume the Codex budget nor require repository-owner authorization under it. + +Automatic Codex review is the initial Codex review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another Codex review after each remediation commit. Within the normal Codex review budget, at most one owner-authorized, delta-scoped Codex verification review may be requested under [the pull-request review workflow](Guidelines/GitHub/PullRequests.md). + + +## Validation + +Run: + +```sh +Scripts/validate_guidelines.swift +``` + +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 new file mode 100644 index 0000000..dcb012e --- /dev/null +++ b/AgentGuidelines/CHANGELOG.md @@ -0,0 +1,304 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [0.0.33] - 2026-09-19 + +### Added + +- Added a Swift Package compiler-settings baseline aligned with the applicable Xcode project warning, Swift language-mode, concurrency, and upcoming-feature policies. +- Added completion-audit enforcement for compiler settings in checked-in Swift packages. + +### Changed + +- Required future Xcode compiler-policy changes to evaluate and update Swift Package Manager parity when an equivalent package setting is applicable. + +## [0.0.32] - 2026-09-13 + +### Changed + +- Made runtime observability an explicit consumer contract for changed stateful, asynchronous, fallible, and lifecycle behavior, while preserving silence for pure values and utilities without meaningful diagnostic boundaries. +- Extended the completion audit to require useful privacy-safe AppLogger outcome coverage instead of accepting dependency declaration and target linkage alone. + +## [0.0.31] - 2026-09-13 + +### Added + +- Added a least-privilege GitHub App authentication pattern for workflows that resolve private sibling repositories, including short-lived read-only tokens, process-scoped Git configuration, exact repository selection, and fork pull-request and self-hosted-runner security boundaries. + +## [0.0.30] - 2026-09-12 + +### Changed + +- Prohibited dedicated License headings and conventional standalone license-description paragraphs in Swift package READMEs while retaining root license files and standard license badges. + +## [0.0.29] - 2026-09-12 + +### Added + +- Added a reusable `.gitignore` template for Xcode, Swift Package Manager, and supported development tooling, plus shared guidance for reviewed project-specific exceptions. + +### Changed + +- Extended the completion audit to reconcile consumer `.gitignore` files with the shared template and escalate undocumented extra patterns for a repository-owner decision. +- Prohibited CocoaPods and Carthage under the external-dependency policy, and prohibited fastlane under CI/CD guidance while requiring first-party Swift or ThatFactory delivery tooling and narrowing Python or shell automation to documented Swift capability gaps. + +## [0.0.28] - 2026-09-09 + +### Added + +- Added a shared version-controlled App Store metadata and app-store-connect-mcp synchronization workflow, including scoped planning, immutable application, reconciliation, screenshot, credential, and submission boundaries. +- Added `ITSAppUsesNonExemptEncryption = NO` to the Xcode application baseline and completion audit, with a documented-exception path for apps that ship non-exempt encryption. +- Added mandatory post-merge cleanup of merged local feature branches after returning to an updated primary branch. + +### Changed + +- Linked localization guidance to the shared App Store metadata workflow while keeping product voice, locales, and concrete storefront content in consumer repositories. + +## [0.0.27] - 2026-09-05 + +### Fixed + +- Preserved valid GitHub alert syntax in the Markdown wrapping audit while continuing to reject hard-wrapped alert bodies, ordinary blockquotes, malformed alert markers, and nested alerts. + +## [0.0.26] - 2026-09-02 + +### Added + +- Added a fail-closed completion-audit helper that requires structured Xcode String Catalog editor evidence for every changed `.xcstrings` file. +- Added native Swift tests for guideline, consumer-integration, Markdown, and String Catalog automation. + +### Changed + +- Moved the shared localization guide from `Guidelines/Swift/` to `Guidelines/` and updated repository and consumer-template links. +- Replaced Python validation, localization, and audit-helper scripts with native Swift executables, and moved CI and release validation to macOS runners. +- Required new repository-owned executable scripts in Swift-focused repositories to use Swift, with the existing `swift_format.sh` wrapper retained as a narrow exception. + +## [0.0.25] - 2026-09-02 + +### Added + +- Added generic generated-symbol localization guidance plus reusable String Catalog preparation and validation scripts with consumer-configured paths and languages. +- Added an Xcode project-settings baseline covering warnings-as-errors, strict and approachable concurrency, default MainActor isolation, the latest stable Swift language mode, and every upcoming feature that remains opt-in for that language mode. +- Added documentation conventions for single-line Markdown prose and aligned ASCII diagrams, together with a deterministic wrapping checker. + +### Changed + +- Expanded the completion audit to verify localization workflows, project-level Xcode setting inheritance and documented exceptions, documentation formatting, and convention adoption across existing durable documentation. +- Updated the consumer template and guideline catalog for the new Xcode project-settings and localization workflows. + +## [0.0.24] - 2026-08-31 + +### Added + +- Added a versioned external-dependency contract to consumer `AGENTS.md` files so applications, games, and reusable packages default to native or ThatFactory-owned implementations and require explicit repository-owner approval plus a durable decision record for third-party product dependencies. +- Extended the completion audit and consumer-setup validation to detect unapproved or undocumented dependency additions and contract drift. + +### Changed + +- Clarified package guidance so first-party packages cannot conceal third-party runtime dependencies and guideline-mandated tooling remains tooling-only. + +## [0.0.23] - 2026-08-26 + +### Added + +- Added a versioned documentation-maintenance contract to the consumer `AGENTS.md` template and consumer-setup validation so guideline upgrades detect projects that have not adopted the contract. + +### Changed + +- Strengthened documentation guidance and the completion audit so durable behavior changes and any change that makes existing documentation stale require documentation updates, while incidental implementation details do not create documentation churn. + +## [0.0.22] - 2026-08-24 + +### Added + +- Required completion audits to verify and repair shared AppLogger integration for ThatFactory Apple-platform applications and Swift packages. + +## [0.0.21] - 2026-08-23 + +### Changed + +- Clarified that the bounded review-round policy applies to Codex GitHub reviews, while otherwise-authorized ChatGPT and Reasoning Relay delegations use their own workflow limits. +- Namespaced Codex review tracking fields and advanced the consumer code-review contract to v2. + +## [0.0.20] - 2026-08-21 + +### Changed + +- Documented squash merge as the default for ThatFactory repositories and instructed agents to use `gh pr merge --squash` instead of attempting merge commits. + +## [0.0.19] - 2026-08-19 + +### Added + +- Added a complete README badge-block example covering Swift, Xcode, platform, package manager, agent/tooling, DocC, license, updated, revision, CI, and publishing badges. + +### Changed + +- Updated this repository's README badges to use the canonical order and applicable tooling, release, and maintenance badges. +- Updated the README contract validator to require the canonical `Xcode MCP` badge alt text. + +## [0.0.18] - 2026-08-18 + +### Changed + +- Corrected the standard README badge order to place DocC/documentation before license, updated date, revision, CI badges, and release/publishing status. + +## [0.0.17] - 2026-08-18 + +### Added + +- A version-marked consumer Code Review contract that defines P0/P1 release blockers, non-blocking P2/P3 observations, and bounded follow-up review scope. +- A deterministic consumer-setup validator for review-contract drift, subtree review scope, `.gitattributes`, local guide links, audit-skill wiring, and Swift-format adoption. + +### Changed + +- Expanded the global Codex instruction template and pull-request workflow to prioritize concrete release risk, group shared root causes, and stop review loops after blockers are resolved. +- Extended the completion-audit skill to verify consumer integration, review convergence, Swift-format configuration, local execution, and non-mutating CI coverage. +- Documented explicit Swift package formatting before tests and added a strict Swift-format CI template with package path coverage. + +## [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/AgentGuidelines/Configurations/Swift/.editorconfig b/AgentGuidelines/Configurations/Swift/.editorconfig new file mode 100644 index 0000000..f3faacc --- /dev/null +++ b/AgentGuidelines/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/AgentGuidelines/Configurations/Swift/.swift-format b/AgentGuidelines/Configurations/Swift/.swift-format new file mode 100644 index 0000000..ee5586f --- /dev/null +++ b/AgentGuidelines/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/AgentGuidelines/Guidelines/AgentWorkflow.md b/AgentGuidelines/Guidelines/AgentWorkflow.md new file mode 100644 index 0000000..8068dd6 --- /dev/null +++ b/AgentGuidelines/Guidelines/AgentWorkflow.md @@ -0,0 +1,57 @@ +# 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. + +## Bounded iteration + +Before starting an iterative review, remediation, or model-assisted refinement loop, define its objective, blocking threshold, round budget, and stop condition. New non-blocking observations do not reset the budget or widen the original objective. + +Do not translate feedback directly into both a change and another review request. Classify the feedback, group items with the same root cause, batch accepted corrections, and rerun only the validation or bounded review needed to verify them. + +Stop when the stated acceptance condition is satisfied. Zero possible comments, improvements, or edge cases is not a valid completion criterion. For pull-request review severity, state tracking, and round limits, follow [GitHub pull requests](GitHub/PullRequests.md). + +## 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/AppStore.md b/AgentGuidelines/Guidelines/AppStore.md new file mode 100644 index 0000000..ec5ed3f --- /dev/null +++ b/AgentGuidelines/Guidelines/AppStore.md @@ -0,0 +1,38 @@ +# App Store metadata + +Use a version-controlled `AppStore/` directory as the source of truth for managed App Store Connect content. Edit repository files first, then synchronize them with ThatFactory's [app-store-connect-mcp](https://github.com/thatfactory/app-store-connect-mcp). Do not maintain managed descriptions, promotional text, screenshots, or review notes through independent browser edits. + +## Repository structure + +Follow the MCP's current [AppStore directory format](https://github.com/thatfactory/app-store-connect-mcp/blob/main/Documentation/AppStore-Format.md). Keep structured values in JSON, prose in UTF-8 text files, and reusable screenshot originals under `AppStore/assets/`. One `AppStore/` root represents one App Store app; require an explicit root when a repository contains more than one app. + +Keep product-specific values in the consumer repository, including the Apple ID, bundle identifier, platforms, version strings, storefront locales, categories, URLs, product copy, review instructions, screenshot sets, and any narrower synchronization commands. Storefront locale identifiers and files are distinct from application String Catalogs, but the consumer's documented voice and terminology apply to both. + +Omission means unmanaged. Add only fields and domains the project intends to reconcile. Do not infer missing translations, version values, categories, availability, capabilities, or release choices from another project. + +## Synchronization workflow + +1. Edit only the requested files and fields in `AppStore/`. Preserve curated content outside the requested scope and obtain the project's required translation review for localized copy. +2. Run `validate_repository` with the explicit App Store root, platform, version, locales, and selected domains. Treat validation as scope-aware: unfinished content outside the intended operation must not expand that operation. +3. Run the applicable planning tool with the same scope. Use `plan_metadata_changes` for listing and review metadata, `plan_screenshot_changes` for screenshot sets, and the corresponding dedicated planner for commerce, provisioning, signing, or submission. Inspect the exact app, platform version, locales, operation IDs, additions, changes, removals, and destructive effects. +4. Apply only an inspected immutable plan within the owner's authorized scope. Supply the exact `planId`, digest, operation IDs, and required host confirmation to `apply_plan`. Plans are process-bound, so keep planning and application in the same MCP server process. +5. If an outcome is uncertain, inspect `get_operation_status` and remote state. Never replay a write whose execution may have started. +6. Plan the same scope again against unchanged repository inputs and require `noOp: true`. Record the reconciliation result alongside the repository change. + +Ordinary metadata, screenshot, commerce, provisioning, or signing synchronization does not authorize submitting a version for review or releasing it. Use the MCP's explicit readiness and submission workflow only when the owner has separately placed submission in scope. + +## Screenshots + +Treat each localization's `screenshots.json` as the ordered manifest for its selected display sets. Paths are relative to `AppStore/`, and the explicit array order is the storefront order. Preserve reusable originals in the repository; do not silently resize, transcode, substitute another locale, or derive ordering from filenames. + +Use `merge` when unrelated remote screenshots must remain unmanaged. Use `replace` only when the selected set should match the manifest exactly, and inspect every planned removal before applying it. After upload, allow Apple processing to finish and reconcile the final remote bytes and order. A temporarily absent source checksum is processing-pending evidence, not proof of a mismatch. + +## Credentials and operational state + +Keep `.appstore-connect-mcp/` as an ignored sibling of `AppStore/`; it contains process and recovery state, not repository content. Never commit or print App Store Connect keys, signing keys, certificates, private contacts, demo credentials, upload URLs, environment files, or MCP journals. Represent supported App Review secrets through the MCP's narrow environment-variable references, and pass credential variable names rather than values when configuring an MCP client. + +If the MCP process does not inherit credentials that are available to an authorized shell, correct or deliberately bridge the process environment without exposing values. Do not diagnose a credential as invalid merely because a different process did not inherit it. + +## Project-specific documentation + +Consumer documentation should identify its concrete `AppStore/` paths and managed domains, explain how product voice and locale coverage apply to storefront copy, describe screenshot production, and record any review, export-compliance, or release prerequisites. Keep generic MCP mechanics in this guide and link to them instead of copying the workflow into every repository. diff --git a/AgentGuidelines/Guidelines/Architecture/Redux.md b/AgentGuidelines/Guidelines/Architecture/Redux.md new file mode 100644 index 0000000..4a28847 --- /dev/null +++ b/AgentGuidelines/Guidelines/Architecture/Redux.md @@ -0,0 +1,297 @@ +# Redux Architecture + +Use this guide for applications that explicitly adopt the ThatFactory Redux architecture. Do not apply it to reusable packages or repositories whose local instructions choose another architecture. + +## Principles + +- Keep one application store as the source of truth for durable application state. +- Views read state and dispatch actions; they do not mutate application state directly. +- Actions describe events or intent, not implementation steps. +- Reducers are pure and synchronous. +- 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 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 + +```text + await dispatch(Action) + +--------------+ --------------------------> +-----------+ + | SwiftUI view | | Store | + | | <-------------------------- | | + +--------------+ observable state | 1. Reducer| + | 2. Middle-| + | ware | + +-----+-----+ <---------------+ + | | + | side effect | + v | + +--------------+ | + | Service | | + | package/API | | + +------+-------+ | + | | + | Action? | + +-----------------------+ +``` + +The store reduces the original action first, then awaits middleware and sequentially dispatches returned follow-up actions. Keep ordering observable and deterministic. Do not start unstructured work inside reducers or hide state changes inside services. + +## Store + +Use one observable store as the source of truth and inject it at the application root. 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: + +1. Reduce the original action. +2. Capture the resulting state. +3. Await each registered middleware with that state and action. +4. Collect returned actions. +5. Dispatch follow-up actions sequentially. + +Use only `await store.dispatch(_:)`. Do not add a fire-and-forget dispatch API. + +## 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: + +```text +/ +|-- App/ +|-- Model/ +|-- Redux/ +| |-- Action/ +| |-- Middleware/ +| |-- Reducer/ +| |-- Selector/ +| |-- State/ +| `-- Store.swift +|-- Services/ +|-- Tools/ +|-- View/ +`-- Resources/ + +Tests/ +|-- Mocks/ +|-- Model/ +|-- Redux/ +| |-- Action/ +| |-- Middleware/ +| |-- Reducer/ +| |-- Selector/ +| `-- State/ +|-- 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: + +```markdown +| Role | Physical folder | +|---|---| +| Redux | `Shared/Redux/` | +| Models | `Shared/Model/` | +| Services | `Shared/Services/` | +| Views | `/View/` | +| Unit tests | `Tests/` | +``` + +Once mapped, use the same component layout beneath those roots. Never guess a destination or create a parallel folder spelling such as `Views/` when the project declares `View/`. + +## Placement rules + +### App + +Put application bootstrap, app delegates, scene definitions, store construction, environment wiring, and root configuration in `App/`. Do not place feature logic there. + +### Model + +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 + +Put domain action enums in `Redux/Action/`. Use a root routing action that wraps focused feature actions: + +```swift +enum AppAction: Equatable { + case account(AccountAction) + case navigation(NavigationAction) +} +``` + +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. + +State stores durable facts. Avoid storing values that are cheap, deterministic derivations unless caching is an explicit measured requirement. + +Sub-states should conform to `Equatable` and `Codable`; add `Sendable` when their values and concurrency boundaries require it. Keep root state and root actions for genuine cross-domain behavior. Keep domain action cases descriptive of intent or outcomes and route them through the root action. + +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: + +- perform asynchronous work; +- call services or packages; +- read the clock or generate random values; +- access files, databases, network clients, or system APIs; +- dispatch actions; +- trigger UI behavior directly. + +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. + +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/ +|-- AccountMiddleware.swift +|-- LoadAccountMiddleware.swift +`-- UpdateAccountMiddleware.swift +``` + +### Selector + +Put pure, reusable domain extraction in `Redux/Selector/`. A selector may answer questions such as the current signed-in account, whether a capability is enabled, or which domain items are visible. + +Do not put SwiftUI types, colors, images, localized display strings, or render-ready screen state in selectors. + +### Services + +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 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. Keep `View/` flat while it has only a few files; introduce `View//` when a familiar feature genuinely has several views. + +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 +``` + +Keep a tiny private projection beside its consuming view only when it is an implementation detail rather than a named value type. + +### Resources + +Put catalogs, assets, preview assets, configuration resources, and test plans in `Resources/` or the concrete resource folders declared locally. Production targets must not depend on test fixtures. + +## File organization + +- 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 + +Create and inject the store at the application root. Views observe only the state they need and dispatch actions for application events. + +Keep view-local interaction state in private `@State` when it is not durable application state. Do not create an `@Observable` view model as a second source of truth for Redux-owned state. + +Prefer narrow view inputs or a focused view-state projection. This aligns SwiftUI invalidation with the smallest useful surface while Redux remains the durable source of truth. + +## Adding a feature + +| Step | Change | Default destination | +|---|---|---| +| 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/` or a feature folder when several | +| 9 | Register middleware | `Redux/Middleware/AppMiddlewares.swift` | +| 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/` 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. + +## Testing responsibilities + +- Reducer tests provide state plus an action and assert the returned state. +- 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. +- 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/AgentGuidelines/Guidelines/CICD.md new file mode 100644 index 0000000..5138aca --- /dev/null +++ b/AgentGuidelines/Guidelines/CICD.md @@ -0,0 +1,104 @@ +# CI/CD + +## Workflow principles + +- Keep CI deterministic, reproducible, and aligned with the repository's supported Xcode, Swift, and platform versions. +- Treat warnings introduced by a change as failures even when the compiler does not. +- Prefer the smallest permissions required by each workflow and job. +- For a new workflow, use the latest stable major version of every GitHub Action available at the time of creation. +- Do not copy an older major version into a fresh workflow unless a documented compatibility constraint requires it. +- For existing workflows, review action release notes and update deliberately rather than allowing runtime deprecation warnings to accumulate. +- Pin third-party actions to an intentional version and review updates. +- Do not place secrets in workflow files, logs, fixtures, or command arguments that may be echoed. +- Keep release workflows separate from pull-request validation when their permissions differ. + +## Tooling and automation + +Fastlane is forbidden in every ThatFactory project and has no exception path. Build repository-owned CI/CD and delivery automation with focused ThatFactory tooling such as `xcode-cloud-mcp` and `app-store-connect-mcp`, plus Swift scripts where repository-specific orchestration is needed. + +Use Swift for new repository-owned executable scripts in Swift-focused applications, games, and packages. Prefer the Swift standard library and Foundation so automation uses the same native toolchain and dependency policy as the codebase. Convenience, familiarity, shorter code, or an existing interpreter is not a reason to choose another language. + +Fall back to Python or a POSIX shell script only when the required behavior cannot be implemented with the repository's supported Swift toolchain and Foundation APIs. Document the exception in durable repository documentation in the same change, including the missing Swift capability, exact script and task scope, runtime and dependency requirements, security and maintenance impact, validation method, and condition for revisiting or removing the exception. Keep the fallback narrow; an existing non-Swift script does not authorize another one. The central `Scripts/swift_format.sh` command wrapper is the retained documented exception for invoking Xcode's `swift-format` modes. + +## Private repository dependencies + +The workflow repository's `GITHUB_TOKEN` does not grant access to private dependencies in sibling repositories. When Swift Package Manager or another build tool must clone private ThatFactory repositories, use a GitHub App installed on every required dependency repository. The app does not need access to the workflow repository unless that repository is also an intended token target. Grant the app only read access to repository contents, mint a short-lived installation token with `actions/create-github-app-token`, and list the exact dependency repositories in the action's `repositories` input. Do not use a personal access token, a long-lived machine credential, or an organization-wide token when the GitHub App can provide the required scope. + +Expose the installation token only to steps that resolve or build the private dependencies. Supply HTTPS authentication through Git's process-level `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_0`, and `GIT_CONFIG_VALUE_0` environment variables so the credential is not persisted in repository or global Git configuration. Keep the existing dependency URLs as `https://github.com//` URLs. For example: + +```yaml +- name: Create private dependency token + id: private-dependencies + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.PRIVATE_DEPENDENCIES_APP_CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_DEPENDENCIES_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: | + first-private-package + second-private-package + permission-contents: read + +- name: Test + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: url.https://x-access-token:${{ steps.private-dependencies.outputs.token }}@github.com/.insteadOf + GIT_CONFIG_VALUE_0: https://github.com/ + run: swift test +``` + +The GitHub App's installation and repository selection are part of the security boundary. Consumer documentation must name the app variable and secret, list the private repositories the workflow requires, record the required `Contents: read` permission, and identify the jobs or steps that receive the token. Keep pull-request and protected-branch workflows consistent unless a documented trust boundary requires otherwise. Because `GIT_CONFIG_*` values are ordinary inherited environment variables, treat the credential-bearing resolve or build step and its complete subprocess tree as privileged. Tests, build scripts, SwiftPM plugins, and other code executed beneath that step must be trusted to receive read access to every repository in the token scope. + +Repository secrets are unavailable to workflows triggered by pull requests from forks. A repository that accepts fork-originated or otherwise untrusted pull requests must keep a secretless validation path or deliberately skip private-dependency jobs with an explicit, documented condition. Untrusted code must not execute on a persistent self-hosted runner that is later reused for credential-bearing work. Use an isolated disposable or ephemeral self-hosted runner, an appropriate GitHub-hosted runner where possible, or a separate runner pool or host that never subsequently receives secrets; otherwise skip the untrusted validation. + +This trust rule is event-independent. Do not combine credentials with code that is not trusted at that privilege level under `pull_request`, `pull_request_target`, `issue_comment`, `workflow_run`, or another trigger. On self-hosted runners, do not expose the token to unrelated steps, caches, artifacts, logs, or persistent configuration; retain the action's default post-job token revocation. + +## `ci-pr.yml` + +Projects using GitHub Actions should keep pull-request validation in `.github/workflows/ci-pr.yml`, triggered by `pull_request` events for `opened`, `synchronize`, and `reopened`. + +Use GitHub-hosted runners for jobs that can run on the hosted operating system and toolchain. When a job uses a self-hosted runner, document and select it through the repository's `Runner labels:` rather than hard-coding a machine name in shared guidance. + +### Runner labels: + +When a workflow uses self-hosted runners, document the labels required by each job in this section of the consumer's CI/CD guide. Always include `self-hosted` and add only stable capability or environment labels needed to select the runner, such as an operating system, architecture, toolchain, or signing capability. Keep machine names and changing fleet details out of shared guidance. + +A typical Swift package validates: + +- package resolution; +- build; +- Swift Testing tests; +- DocC generation when the package publishes documentation; +- repository-specific lint or validation scripts. + +An Xcode application validates its declared scheme and test plan. Use the same project/workspace, configuration, and platform assumptions documented for local development. + +Xcode projects and Swift packages must run on self-hosted macOS runners with the required Xcode, Swift toolchains, simulators, certificates, and signing environment. Do not use `macos-latest` for those jobs. For Xcode projects, test with `xcodebuild test` and explicit simulators, then validate compilation with `xcodebuild build CODE_SIGNING_ALLOWED=NO` across the supported platforms. For Swift packages, use Swift Package Manager commands such as `swift test` and `swift build`; packages do not require simulator selection, but may require the self-hosted signing environment for packaging or collection workflows. Generic jobs that do not require Apple tooling may use GitHub-hosted Linux or other suitable runners. CI validates tests and compile health, not app-store distribution. + +## `ci.yml` + +Validation of merges to `main` should live in `.github/workflows/ci.yml`, triggered by `push` on `main`. Use the same build, test, lint, and platform coverage as pull-request validation unless the repository documents a deliberate difference. + +## Failure investigation + +1. Use GitHub MCP connector tools to inspect check runs and logs for the failing commit or pull request. +2. Use `gh` for fast local triage when needed. +3. Reproduce locally with the exact build or test command shown in the failing job logs. + +Useful commands: + +```bash +gh run list --limit 10 +gh run view +gh run view --log +``` + +Distinguish compiler errors from lint violations, test failures from simulator or runtime infrastructure failures, and single-job failures from cross-platform matrix failures. Identify the first meaningful failing step, fix the narrowest root cause, and re-run affected validation. + +## Releases + +- A release tag and GitHub release must match the intended semantic version. +- Release notes summarize user- or integrator-relevant changes since the previous release. +- Use a notes file for multiline CLI release descriptions. +- Do not publish a release from an unverified or dirty worktree. +- Follow the consumer's local instructions for deployment, signing, notarization, App Store, or documentation publishing steps. diff --git a/AgentGuidelines/Guidelines/Development.md b/AgentGuidelines/Guidelines/Development.md new file mode 100644 index 0000000..0c61a9f --- /dev/null +++ b/AgentGuidelines/Guidelines/Development.md @@ -0,0 +1,56 @@ +# Development + +## Reusability first + +When developing a new feature or responding to a feature request, consider shared code first. If the code fits an existing package, suggest extending that package instead of adding the implementation directly to an application. Also consider whether the change belongs in a new Swift package, even when that package does not exist yet. Prefer reusable, focused package APIs when they can serve more than one consumer. + +## External dependencies + +ThatFactory applications, games, and reusable packages are first-party by default. Do not introduce a new third-party source or binary dependency during normal feature development. Prefer Apple platform APIs, the Swift standard library, code owned by the current repository, or a focused ThatFactory-owned package. If reusable capability is missing, implement it natively at the appropriate boundary and consider extracting it into a first-party package when it can serve multiple consumers. + +Do not add a third-party dependency merely to save implementation time, reduce code volume, or avoid learning a platform API. Do not make an external dependency acceptable by hiding it behind a first-party wrapper. + +An exception requires explicit approval from the repository owner for the specific dependency and use before changing the dependency graph. If approved, record the decision in durable repository documentation in the same change, such as the architecture document or an ADR. Record the dependency and source, purpose and target scope, why a native or first-party implementation is not appropriate, relevant license, security, and maintenance considerations, version or update policy when material, and the approval context. Do not infer approval from an execution plan, pull-request description, agent choice, or transient chat that merely mentions or uses the dependency. Regardless of where explicit approval occurs, reflect the exception in durable repository documentation. + +An existing approved dependency does not authorize a different dependency. Expanding an existing third-party dependency to a new target or runtime role requires the same approval and documentation. Routine version updates that do not change the approved role remain governed by the intentional consumer-update workflow in [Packages](Packages.md). + +For this policy, a third-party dependency is externally maintained source or binary code linked into or shipped with the product, including externally maintained Swift packages, vendored libraries, frameworks or XCFrameworks, CocoaPods, Carthage dependencies, and equivalent runtime libraries. Apple system frameworks and the Swift standard library are platform dependencies, not third-party dependencies. Packages maintained by ThatFactory are first-party dependencies. + +Tooling dependencies explicitly required by these shared guidelines, such as documentation or build plugins used only by tooling, are pre-approved for that documented role. They must not be linked into or shipped with product runtime targets unless the repository owner separately approves and documents that use. + +CocoaPods and Carthage are forbidden in every ThatFactory project and are not eligible for the exception process above. Use Swift Package Manager for package dependencies. + +## Guidelines version + +Before changing a project, verify that it uses the latest released version of `agent-guidelines`. Check the project's `AgentGuidelines/VERSION` against the latest release, update the subtree or equivalent when it is behind, and read the updated applicable guides before starting implementation. This check is manual and must be performed at the beginning of each project task. + +## Guidelines changes in pull requests + +Keep `AgentGuidelines/` tracked so consumers retain a reproducible, versioned copy for agents and CI. Do not add the subtree to `.gitignore`. Instead, add this rule to the consumer's tracked `.gitattributes` so GitHub collapses synchronized guideline files in pull-request diffs by default while reviewers can still expand them: + +```gitattributes +# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. +AgentGuidelines/** linguist-generated +``` + +Keep each subtree update in its own commit. In the pull-request description, state the old and new guideline versions and link to the central release or pull request where the guideline changes were reviewed. Continue validating the checked-in subtree in CI. Because generated-file diffs are collapsed by default, never edit the subtree locally; make shared changes in the source repository and consume a tagged release. + +## 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. + +For subtree consumers, the audit runs `AgentGuidelines/Scripts/validate_consumer_setup.swift` to detect drift in the root Code Review and Documentation Maintenance contracts, Codex subtree-review scope, `.gitattributes`, local guide links, and repository skill symlink. When the root `AGENTS.md` links the shared Swift-format guide, the validator also requires the shared configuration symlinks and strict non-mutating CI adoption. User-level global Codex instructions are outside this repository audit. + +## Post-merge cleanup + +After an in-scope feature pull request merges, switch the original checkout back to the repository's primary branch, update it from its upstream with a fast-forward-only pull, and delete the merged local feature branch. Confirm the remote feature branch is absent when the repository deletes merged branches automatically. Do this before declaring the feature workflow complete so stale branches do not accumulate. + +Never discard unrelated changes to perform cleanup. If the original checkout is dirty, another worktree still uses the feature branch, the merge did not complete, or the primary branch cannot fast-forward, leave the branch intact and report the exact blocker. Do not use force deletion merely to hide an unmerged branch. + +## 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. + +Treat observability as part of implementing or changing stateful, asynchronous, fallible, or lifecycle-oriented behavior. Before handoff, trace those boundaries and verify that privacy-safe AppLogger events distinguish the outcomes needed to diagnose the behavior in context. Merely adding the dependency or linking its product is not sufficient. Keep pure value and utility code silent when it has no meaningful event boundary, and record that deliberate decision in the handoff rather than manufacturing noisy logs. diff --git a/AgentGuidelines/Guidelines/Documentation.md b/AgentGuidelines/Guidelines/Documentation.md new file mode 100644 index 0000000..2d8325c --- /dev/null +++ b/AgentGuidelines/Guidelines/Documentation.md @@ -0,0 +1,78 @@ +# Documentation + +Documentation is part of implementation. For every codebase change, evaluate whether durable project knowledge or any existing documentation is affected; do not treat documentation as optional cleanup after code and tests are complete. + +## Conventions + +- Use PascalCase Markdown filenames without spaces. +- Keep the folder flat until one topic genuinely requires several files. +- Prefer current implementation over speculative future design; label known gaps explicitly. + +### Line wrapping + +**Write each paragraph as a single physical line — do not hard-wrap prose at a fixed column width.** A paragraph hand-wrapped at ~100 columns shows up with breaks mid-sentence, which looks broken. Let your editor **soft-wrap** instead of inserting newlines. + +The same rule applies to multi-sentence **list items** and **blockquotes** — keep each item/quote on one line. GitHub alerts are the exception: keep the alert marker on its required quoted line and keep each following body paragraph on one physical quoted line. Do not nest alerts. This concerns **prose only**: fenced code blocks and ASCII diagrams are published verbatim, so wrap those exactly as they should appear (one line per row). + +### Diagrams + +Use ASCII art inside fenced code blocks: + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ View │──────>│ Store │──────>│ Service │ +└──────────┘ └──────────┘ └──────────┘ +``` + +Use box-drawing characters (`─`, `│`, `┌`, `┐`, `└`, `┘`) and arrows (`──>`, `<──`, `v`, `^`). + +#### Keep diagrams aligned + +Diagrams are published verbatim, so a diagram that is misaligned in the repo will look misaligned on MD readers/editors. Misalignment is also the most common defect in these files — it creeps in when someone edits a label without re-padding the rest of the row. + +- **Every vertical rule must sit in the same character column on every row.** Decide the column positions up front, then pad each row with spaces to hit them. In a sequence diagram, each participant's lifeline (`│`) is one such column. +- **A box's interior width must match its border width.** `┌──────────┐` (10 dashes) needs exactly 10 characters between the `│` on the rows below it. +- **Put arrowheads beside the target rule, not on top of it** — `│─────>│`, so the lifeline stays unbroken. An arrow that spans intermediate participants simply passes through their columns. +- **When a label is too long for its cell, wrap it onto a second row** rather than letting it push the rules out of alignment. +- **Count characters, not bytes.** Box-drawing glyphs are 3-byte UTF-8, so `wc -c` and `awk '{print length}'` report well above the visual width — around 2× for a typical row, up to 3× for one that is mostly box-drawing. Measure with `swift -e 'import Foundation; let value = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""; print(value.split(separator: "\n", omittingEmptySubsequences: false).map(\.count).max() ?? 0)' < FILE` or an editor's column indicator. + +Check the result in a monospace view before committing: scan down each vertical rule and confirm it never jogs left or right. For a large or heavily edited diagram, it is quicker and safer to generate the block from a list of column positions in a throwaway script than to count spaces by hand. + +## Code-level documentation + +- 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. +- Add a short Swift example when it materially clarifies correct use. +- Keep documentation close to the declaration it describes. + +## Project-level documentation + +- Keep durable architecture and cross-cutting guides in the consumer's declared documentation folder. +- Update project documentation when a change alters durable or core feature behavior, architecture, data flow, public API, persistence, navigation, localization process, testing workflow, delivery workflow, configuration, or another documented contract. +- Regardless of change size, update or remove existing documentation when the implementation makes a documented statement inaccurate, incomplete, misleading, or obsolete. +- Do not create broad documentation for incidental implementation details that are neither durable knowledge nor already documented. A small change that leaves durable knowledge and existing documentation accurate needs no project-level documentation edit. +- When renaming or removing behavior, search durable documentation for old names, examples, defaults, diagrams, setup steps, and references that may now be stale. +- Keep investigations, temporary plans, and one-time spike notes out of durable documentation unless they become lasting guidance. +- Prefer ASCII diagrams in fenced code blocks when universal rendering matters. + +## Review checklist + +When reviewing a change, ask: + +- Which existing documentation describes the changed feature, API, configuration, workflow, or invariant? +- Does the change alter durable or core feature behavior? +- Would any existing statement become inaccurate, incomplete, misleading, or obsolete even if the implementation change is small? +- Does it introduce a reusable architectural pattern? +- Does it change data flow, ownership, persistence, localization, testing, or delivery? +- Does it remove or supersede an existing guide? +- Are code comments and project guides consistent with the implementation? +- If no documentation changed, is that because no durable knowledge changed and no existing documented claim was affected? + +Treat known stale documentation as incomplete implementation. Avoid documentation churn when the change neither affects durable knowledge nor changes an existing documented claim. + +## Shared versus local guidance + +This repository owns reusable policy. Consumer documentation owns its product domain, concrete paths, package relationships, feature registries, and explicit exceptions. Link across those layers instead of copying shared prose locally. diff --git a/AgentGuidelines/Guidelines/Git/IgnoreFiles.md b/AgentGuidelines/Guidelines/Git/IgnoreFiles.md new file mode 100644 index 0000000..d1f7a26 --- /dev/null +++ b/AgentGuidelines/Guidelines/Git/IgnoreFiles.md @@ -0,0 +1,15 @@ +# Git Ignore Files + +Use the shared [`.gitignore` template](../../Templates/.gitignore) for new Xcode projects and Swift packages. It covers generated Xcode and Swift Package Manager state plus the local output, caches, and credentials used by supported Node.js, Python, App Store, and ThatFactory development tooling. + +The template deliberately does not ignore `Package.resolved`, `.xcodeproj`, or `.xcworkspace` files. Decide whether a package lockfile belongs in source control based on the package's role, and keep authored Xcode project and workspace definitions tracked. Narrow generated files are ignored instead of ignoring containers that may hold shared configuration. + +## Consumer reconciliation + +Compare a consumer's active `.gitignore` patterns with the template without requiring comments, blank lines, section order, or duplicate rules to match. Add missing shared patterns using their template spelling, preserving the consumer's rule order and negation semantics. Before adding a pattern, check whether a later consumer rule negates it or whether it would hide a currently tracked source or configuration path; report a conflict instead of changing behavior silently. + +Treat every active consumer pattern that is not in the template as a project-specific entry. Do not remove it or silently accept it. Report it to the repository owner so they can choose whether the reusable template should gain the pattern or the consumer should retain it as a local exception. + +When the repository owner chooses a local exception, document the rationale immediately above the entry in the consumer `.gitignore` using `# Project-specific: `. A concrete non-placeholder rationale marks that entry as reviewed, so later completion audits may accept it without reporting it again. Keep exception comments and their patterns adjacent; each independently motivated group needs its own rationale. + +Do not copy ignored caches or generated artifacts into the repository merely to make the comparison pass. Existing tracked files remain tracked even when a matching ignore rule is added, so inspect `git ls-files` and `git check-ignore -v` when a rule's effect is uncertain. diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/AgentGuidelines/Guidelines/Git/Repositories.md new file mode 100644 index 0000000..04e66ce --- /dev/null +++ b/AgentGuidelines/Guidelines/Git/Repositories.md @@ -0,0 +1,52 @@ +# Git Repositories + +Use these rules when cloning repositories or configuring remotes. + +## SSH-first cloning + +Clone a repository over SSH when the working copy may be used to commit, push, or open a pull request: + +```sh +git clone git@github.com:/.git +``` + +- Prefer an SSH `origin` so command-line tools and Git clients such as Fork can reuse the machine's GitHub SSH authentication. +- Do not create a push-capable working copy with an HTTPS `origin` unless the user or environment explicitly requires HTTPS. +- After cloning for development, use `git remote -v` to confirm that fetch and push URLs are correct. +- If an existing development clone has an HTTPS `origin`, change it only when requested or when the task explicitly includes remote setup: + + ```sh + git remote set-url origin git@github.com:/.git + ``` + +HTTPS remains appropriate for deliberately read-only retrieval, ephemeral automation, or environments where SSH credentials are unavailable. A public Git subtree remote may also remain HTTPS because consumers fetch tagged content without pushing to the guideline repository. + +## GitHub CLI authentication recovery + +Treat a reported invalid `GITHUB_TOKEN` as potentially transient or environment-specific. Do not abandon the `gh` CLI or switch protocols solely because one Codex shell reports that token as invalid. + +When `gh` authentication appears inconsistent: + +1. Retry `gh auth status` in a fresh shell. +2. If the user can run commands locally, ask them to confirm `gh auth status` and share only the redacted result; never request or print the token itself. +3. Retry the original `gh` command after authentication is confirmed. Preserve the CLI workflow for repository inspection, Actions logs, and pull-request operations. +4. If an injected environment variable is shadowing the stored GitHub CLI credential, compare the credential-backed check without exposing secrets: + + ```sh + env -u GITHUB_TOKEN gh auth status + ``` + + If that succeeds, use the authenticated CLI session for the task or refresh it with `gh auth refresh` as appropriate. Do not copy a token into shell history, command arguments, files, or chat. +5. Use SSH for Git transport only when the CLI remains unavailable after retry and the operation is specifically a Git fetch, commit, or push. Continue using `gh` for GitHub API operations whenever it is working. + +An environment mismatch is not evidence that the user's GitHub account or token is invalid. Record the failed command and exact non-secret error, retry after the authentication check, and report the blocker only after repeated attempts fail. + +### 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/AgentGuidelines/Guidelines/GitHub/PullRequests.md new file mode 100644 index 0000000..48b85df --- /dev/null +++ b/AgentGuidelines/Guidelines/GitHub/PullRequests.md @@ -0,0 +1,184 @@ +# GitHub Pull Requests + +Use this guide whenever creating, reviewing, updating, or merging a GitHub pull request. + +## Before opening + +- Review the complete diff and exclude unrelated changes. +- Keep each pull request to a coherent review unit with a bounded set of invariants. Split changes that combine independent architecture, persistence, security, transport, and CI concerns when they can be reviewed and delivered separately; do not split merely to minimize line count. +- State the supported use cases, explicit acceptance criteria, and relevant threat model for behavior whose review priority depends on those boundaries. +- For security guarantees based on enumerating formats or signatures, define the finite coverage contract and residual risk, or use a systemic boundary that enforces the guarantee without exhaustive enumeration. +- 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 + +When reviewing a consumer pull request, do not review or comment on files under `AgentGuidelines/**` after exact tagged-tree provenance has been verified. The subtree is a tracked, synchronized copy marked `linguist-generated`; substantive guideline changes are reviewed in the central `thatfactory/agent-guidelines` pull request. Verify `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 pull request instead. + +## Review objective + +Automated review identifies release-blocking regressions; it does not attempt to eliminate every possible improvement. + +Classify findings by impact and reachable scope: + +- **P0 — critical:** an actively exploitable critical security issue, catastrophic durable data loss, or critical production outage. +- **P1 — blocking:** a supported use case, explicit acceptance criterion, or documented threat-model boundary has a concrete reachable failure path that causes a security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, or a serious compatibility regression. +- **P2 — non-blocking:** robustness, defense-in-depth, bounded edge cases, malformed state that trusted code cannot produce, unsupported scenarios, theoretical completeness, or useful hardening. +- **P3 — non-blocking:** style, naming, preferred refactoring, documentation polish, or optional test improvements. + +Only unresolved P0 and P1 findings block merge. A finding may be technically correct without being release-blocking. + +## Review gate + +Opening a pull request starts review; it does not authorize merging it. + +1. Wait for the configured Codex review to finish. No review yet means pending, not approved. +2. Record the reviewed head SHA and inspect all review summaries, inline threads, checks, and requested changes. +3. Assess each comment for technical correctness, severity, supported reachability, and root cause. +4. Give every thread one explicit disposition: `BLOCKER-P0`, `BLOCKER-P1`, `DEFER-P2`, `DEFER-P3`, `DECLINE`, or `DUPLICATE`. +5. Batch accepted P0/P1 corrections into one remediation pass and add regression coverage where reasonably possible. Lower-severity improvements may be included when they are small and clearly in scope, but they do not keep the review loop open. +6. Reply in the original thread with the disposition and either what changed or the concise technical reason for deferring, declining, or grouping it. +7. Resolve a thread only after its disposition is recorded. Reference a follow-up issue for deferred work when its value justifies one. +8. Rerun affected validation, then update the pull-request description so it matches the current implementation, validation, deferred work, and remaining limitations. +9. Recheck the pull request immediately before merge for late P0/P1 findings and check-state changes. + +When replying with a commit reference, write the commit hash as raw text without backticks (for example, the hash 185c04f should remain 185c04f). GitHub then auto-links the hash to the commit. + +A thumbs-up or clean Codex review satisfies the agent-review step, but it does not replace any human approval required by the repository. Do not enable auto-merge before all review gates are satisfied. + +### Codex review state and round budget + +This round budget applies only to Codex GitHub reviews: the configured automatic Codex review and any manual `@codex review` request. It does not apply to ChatGPT review or reasoning delegated through Reasoning Relay. An otherwise-authorized Reasoning Relay workflow may request as many Relay review or follow-up delegations as its own governing workflow requires; those requests neither consume this Codex budget nor require repository-owner authorization under it. Do not block an agentic goal waiting for a Codex-budget exception before issuing an otherwise-authorized Reasoning Relay request. + +Track enough Codex-review state to prevent duplicate requests and unbounded Codex review loops: + +```text +codex_initial_review_sha +codex_last_reviewed_sha +codex_review_requested_sha +codex_review_round +codex_pending_review +codex_unresolved_p0 +codex_unresolved_p1 +codex_deferred_findings +``` + +The automatic Codex review is the one initial full Codex review. Do not request another Codex review after each fix. A repository owner may explicitly authorize at most one delta-scoped Codex verification review after the known P0/P1 findings have been batch-remediated. + +Before sending that Codex request, verify that no Codex review is pending, no existing request targets the current head SHA, the current head differs from `codex_last_reviewed_sha`, and the Codex verification-round budget is unused. Persist `codex_review_requested_sha`, increment `codex_review_round`, and mark `codex_pending_review` before waiting for a result so a retry cannot submit a duplicate request. + +When authorized, scope the Codex verification request explicitly: + +```text +@codex review only unresolved P0/P1 findings and changes since . +Do not search unchanged code for new P2/P3 issues. +``` + +Do not request a third Codex review or restart a full Codex review without separate, explicit repository-owner authorization and a named unresolved P0/P1 concern. This restriction does not cap Reasoning Relay/ChatGPT review delegations. A new finding in Codex verification must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +Stop the review loop when no unresolved P0/P1 finding remains, every thread has an explicit disposition, required checks pass, and required human authorization is present. Zero comments, zero possible improvements, and zero technical debt are not completion criteria. + +### Codex review monitoring + +Use GitHub review data, reactions, and checks together. An eyes reaction means Codex is processing the pull request; it is not an approval. A thumbs-up means the review completed without suggestions. A submitted review means its inline threads must be assessed individually. + +```text +PR opened at stable head + | + v +One automatic full review + | + +--> thumbs-up ----------------> No P0/P1 blockers + | + `--> Review comments ----------> Classify and group + | + batch P0/P1 fixes + | + owner-authorized delta review? + | | + no yes + | | + stop one verification pass + | + no unresolved P0/P1 + | + stop +``` + +When using the GitHub CLI, monitor all three surfaces: + +```sh +gh api --paginate repos///issues//reactions +gh pr view --repo / --json reviews,headRefOid +gh pr checks --repo / +``` + +Retrieve inline review threads and their resolution state through GraphQL; top-level pull-request comments do not include this information: + +```sh +gh api graphql --paginate \ + -f query='query($owner: String!, $repository: String!, $number: Int!, $endCursor: String) { + repository(owner: $owner, name: $repository) { + pullRequest(number: $number) { + reviewThreads(first: 100, after: $endCursor) { + nodes { id isResolved } + pageInfo { hasNextPage endCursor } + } + } + } + }' \ + -F owner= \ + -F repository= \ + -F number= +``` + +For every unresolved thread identifier returned above, retrieve its complete comment history with a second paginated query: + +```sh +gh api graphql --paginate \ + -f query='query($thread: ID!, $endCursor: String) { + node(id: $thread) { + ... on PullRequestReviewThread { + comments(first: 100, after: $endCursor) { + nodes { id author { login } body url } + pageInfo { hasNextPage endCursor } + } + } + } + }' \ + -F thread= +``` + +Continue polling only while an allowed review round is pending. Inspect every returned page for reactions, review threads, and thread comments. Do not treat missing comments, a pending reaction, truncated results, or elapsed time as review completion, and do not submit a duplicate request merely because polling has not completed. + +## Merge method + +ThatFactory repositories use squash merges by default. Do not attempt a merge commit; GitHub rejects that method in these repositories, and retrying with squash wastes execution time and tokens. Use the GitHub UI or `gh pr merge --squash` after all review, approval, and check requirements are satisfied. Use another merge method only when the repository explicitly allows it and the owner authorizes the exception. + +## Merge requirements + +Do not merge while any of the following is true: + +- Codex review is still pending; +- an unresolved P0/P1 finding remains; +- a review thread lacks an explicit disposition or remains unresolved; +- a required check is pending or failing; +- the branch is out of date when the repository requires an up-to-date branch; +- required human approval or explicit owner authorization is missing. + +## Late findings + +If a review arrives after merge, assess and disposition its findings. A valid late P0/P1 finding requires prompt remediation through a corrective pull request and indicates that a review gate was missed. A late P2/P3 observation becomes backlog work when useful and is not by itself a process failure. + +## Repository protection + +Prefer GitHub rulesets or branch protection for the default branch. At minimum: + +- require changes to arrive through a pull request; +- require conversations to be resolved before merging; +- require the repository's mandatory status checks; +- prevent bypass except for an intentional emergency path. + +A formal one-approval rule works only when someone other than the pull-request author can submit an approving review. In a solo repository where the owner account also authors pull requests, use a bot or service account for authored changes before requiring owner approval; GitHub does not count self-approval. Until that separation exists, require explicit owner authorization operationally and keep conversation resolution enforced technically. diff --git a/AgentGuidelines/Guidelines/Localization.md b/AgentGuidelines/Guidelines/Localization.md new file mode 100644 index 0000000..4cb53f1 --- /dev/null +++ b/AgentGuidelines/Guidelines/Localization.md @@ -0,0 +1,94 @@ +# Localization + +Follow Apple's [Localizing your app using agents](https://developer.apple.com/documentation/xcode/localizing-your-app-using-agents) workflow and current Xcode localization tools. Consumer repositories declare their supported languages, catalog and source locations, product voice, terminology, and narrow exceptions locally. + +Storefront metadata uses separate App Store locale identifiers and files. Follow [App Store metadata](AppStore.md) for the repository structure and App Store Connect synchronization workflow; apply the consumer's local voice and product terminology to both application and storefront copy. + +## Source artifacts + +- Use the consumer's existing String Catalogs (`.xcstrings`) as the source of truth. +- Do not create a parallel catalog or migrate an existing `.strings` setup unless the task includes that migration. +- An app target uses its main bundle by default. Swift packages and frameworks must resolve localized resources from their own bundle, using the current Apple-recommended bundle API. +- Keep one source of truth for translator context: either the source comment or the catalog comment. + +## Generated symbols + +- Define user-facing text in the appropriate String Catalog first and use Xcode-generated `LocalizedStringResource` symbols from Swift. Enable Generate String Catalog Symbols when an older project does not already generate them. +- Give a string a semantic catalog key when deriving a readable symbol from its source value would be ambiguous. Name formatted variables in the source value so Xcode generates labeled parameters. +- Use generated properties and functions directly in SwiftUI, and resolve them with `String(localized:)` or `AttributedString(localized:)` only where that concrete value is required. +- Generated Swift is build output. Inspect it through Xcode or `xcstringstool` when useful, but never edit or check it in. +- Do not add new localizable Swift literals after a project adopts generated symbols. Use `Text(verbatim:)` only for nonlinguistic punctuation, identifiers, and other intentionally unlocalized content. + +## User-facing values + +- Let SwiftUI's localized string initializers preserve localization context. +- Use `LocalizedStringResource` when a model, view state, notification, or other non-view value carries user-facing text that should resolve later. +- Use `String(localized:)` when a resolved localized `String` is genuinely required outside SwiftUI. +- Use `Text(verbatim:)` for intentional non-localized literals such as debug identifiers. +- Do not pass a runtime `String` to a localized initializer and expect Xcode to extract it as a catalog key. + +## Sentences and formatting + +- Interpolate values into one localizable sentence rather than concatenating translated fragments. +- Add translator comments for ambiguous language and describe interpolated placeholders by position and meaning. +- Use locale-aware `FormatStyle` APIs for dates, numbers, lists, measurements, and currencies. +- Avoid runtime case transformations for localized interface text; allow translations to choose appropriate casing. +- Keep placeholder positions, semantic names, and conversion types identical across source and translated variants. +- Add language-specific plural variants for counts instead of branching between singular and plural text in Swift. +- Preserve the distinction between unavailable data and numeric zero. + +## Layout + +- Use leading and trailing instead of left and right for directional layout. +- Avoid fixed text frames that cannot accommodate translation length or script height. +- Prefer semantic text styles to fixed point sizes. +- Use the SwiftUI environment locale for view behavior that must respond to preview or subtree locale overrides. + +## Agent workflow + +1. Inspect the consumer's local localization instructions and catalogs. +2. Ask Xcode's current documentation or localization capability for the supported workflow. +3. Add the key, source-language value, and translator comment directly to the catalog. Mark an explicitly maintained generated-symbol entry as manual and use named placeholders for formatted values. +4. Inspect the generated API through Xcode's catalog inspector or, when needed, `xcrun xcstringstool generate-symbols`. Use the generated property or function from Swift. +5. For a legacy catalog whose keys were extracted from Swift literals, run the shared preparation script once. It preserves comments, variants, and translations, leaves stale entries for an explicit decision, and refuses to invent a missing source value for an already-manual semantic key. Use `--check` for a nonmutating readiness check. +6. Use Xcode's localization coordinator and String Catalog tools to add or update only the languages in scope. Do not replace contextual translation decisions with ad hoc JSON-rewriting scripts. +7. Resolve every stale extracted entry and every active translated value marked `new` or `needs_review`. Keep agent output machine-translated until a fluent reviewer approves it. +8. Run the shared catalog validator after every catalog or localization-source change. It checks generated-symbol readiness, stale entries, required languages, translation states, format signatures, and Swift literals that bypass generated symbols. +9. Open each changed catalog in Xcode and require zero catalog-editor errors or warnings; these diagnostics do not necessarily become compiler warnings. Record the inspected catalog paths, selected Xcode version and build, and explicit zero-error and zero-warning result. Automated catalog validation, a warning-clean build, or an unrecorded visual check does not satisfy this editor-evidence gate. +10. Build and test source and translated languages. Exercise long strings, every plural branch, and right-to-left layout even when no right-to-left locale ships. +11. Have a fluent reviewer inspect machine translations before recording them as reviewed. + +## Shared scripts + +Supply project-specific paths and supported languages at the consumer boundary: + +```sh +AgentGuidelines/Scripts/prepare_localizable_symbols.swift \ + \ + --check + +AgentGuidelines/Scripts/validate_string_catalogs.swift \ + --catalog-directory \ + --source-directory \ + --required-language +``` + +Repeat directory, catalog, or language options when the project has several. A consumer may keep a small repository-owned wrapper so existing developer and CI commands supply its paths and languages consistently; the wrapper must delegate to the synchronized shared script rather than copy its validation or migration logic. + +The completion audit discovers changed String Catalogs relative to an explicit Git base and fails closed until every catalog has a recorded Xcode editor inspection. After opening each changed catalog and confirming zero editor errors and warnings, run the audit helper from the consumer root and keep its JSON outside the repository: + +```sh +AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift \ + --base-ref \ + --inspected-catalog \ + --evidence-output +``` + +Repeat `--inspected-catalog` for every changed catalog. The helper records the selected Xcode version and build together with the zero-diagnostic result. Summarize that record in the completion handoff and pull-request description; do not commit the temporary JSON evidence. + +Do not invent translations from an unrelated project's conventions. Product vocabulary and tone remain consumer-specific. + +## References + +- [Using generated localizable symbols in your code](https://developer.apple.com/documentation/xcode/using-generated-localizable-symbols-in-your-code) +- [Localizing your app using agents](https://developer.apple.com/documentation/xcode/localizing-your-app-using-agents) diff --git a/AgentGuidelines/Guidelines/Logging.md b/AgentGuidelines/Guidelines/Logging.md new file mode 100644 index 0000000..4cf3bb3 --- /dev/null +++ b/AgentGuidelines/Guidelines/Logging.md @@ -0,0 +1,76 @@ +# Logging + +Use this guide for Apple-platform applications and Swift packages that emit runtime diagnostics. Logging should improve observability without changing behavior, exposing sensitive data, or overwhelming the console. + +## Ownership + +- Each application or package owns the logs for operations it implements. +- A consuming application logs its own orchestration and lifecycle events. It must not reproduce or reformat a dependency's internal steps or outcomes. +- A reusable package describes events using its own domain language. Do not introduce concepts from one current client into package categories or messages. +- Ownership does not require every API or package to emit logs. Pure utilities and operations without a meaningful diagnostic event may emit nothing. +- New or changed stateful, asynchronous, fallible, or lifecycle-oriented behavior must identify its meaningful diagnostic boundaries during implementation. Emit a privacy-safe outcome for success and each operationally distinct failure, cancellation, recovery, or state transition that a developer needs to distinguish. If an artifact is limited to pure values or utilities and has no such boundary, keep it silent and state that decision in the implementation handoff instead of adding initializer or property-access noise. +- Logging is a side effect. It must not affect returned values, state transitions, error handling, or control flow. +- Architectures that isolate side effects must call a logging package from an allowed side-effect boundary, such as middleware or a service, rather than from a pure reducer. + +## AppLogger and identity + +- Use the shared [AppLogger Swift package](https://github.com/thatfactory/applogger) rather than `print`, direct `Logger` instances, or project-specific logging backends that duplicate it. +- Add the package's `AppLogger` library product to each target that emits logs. Follow the package's current integration instructions for dependency configuration and version requirements. +- Give each artifact an explicit, stable, lowercase subsystem in reverse-DNS form: `com.thatfactory.`. +- A package always uses its own subsystem, even when its code runs inside a consuming application. This allows filtering all ThatFactory logs or one artifact independently. +- Choose stable categories from the artifact's reusable domain. Categories are not a global vocabulary: a language-evaluation package might use `evaluation`, a progression engine might use `progression`, and an application might use `session` or `lifecycle`. +- Keep the category set as small and generic as possible while still distinguishing meaningful operations within that artifact. +- Do not add a category solely because one current client uses that concept. + +## Package emoji + +- Every log message emitted by a ThatFactory package starts with that package's canonical emoji followed by one space. +- Use the emoji registered in the [ThatFactory Swift Package Collection](https://github.com/thatfactory/swift-package-collection). +- Declare the selected emoji in the package's local instructions or documentation. +- Route package logging through one package-local gateway that owns the subsystem, categories, and emoji prefix. Production call sites must not construct unprefixed package messages directly. + +## Message design + +- Keep each message short, direct, and on one line. +- Prefer one completion or outcome message over separate start, intermediate, and completion messages. +- Use a compact action followed by stable `key=value` metadata when context is useful: + + ```text + evaluate | type=classification, correct=true, score=10 + ``` + +- Do not log routine property access, initializers, collection iterations, or other high-frequency implementation details. +- Use `.debug` for routine diagnostics, `.info` or `.default` for meaningful lifecycle events, `.error` for failures, and `.fault` only for severe conditions that indicate a system-level problem. +- Do not prepend the current time or date. Apple unified logging already records the event timestamp. +- Use AppLogger's `Date.formattedLogTimestamp()` and `TimeInterval.formattedLogDuration()` only when a domain date or elapsed duration is part of the event itself. + +## Privacy + +- Never log credentials, tokens, secrets, personal data, prompts, submitted answers, or other user-generated content as public metadata. +- Prefer omitting sensitive values. If a diagnostic genuinely requires them, mark the entire AppLogger message private. +- Do not emit complete models, collections, or application-state snapshots in routine logs. +- Any temporary state snapshot must be debug-only, explicitly enabled, and private. + +## Testing + +- Keep message rendering independently testable through an internal formatter, injectable sink, or similarly narrow seam. +- Verify that every package message starts with its canonical emoji. +- Verify the stable category, meaningful fields, privacy choice, log level, and single-emission behavior for each logged operation. +- Do not make tests depend on querying the operating system's persisted log store. +- For every changed lifecycle covered by the observability requirement, test the event formatter or sink for its successful and operationally distinct unsuccessful outcomes. Dependency declaration and target linkage alone do not establish logging coverage. + +## Filtering + +Use the subsystem in Console or the macOS `/usr/bin/log` command. For example: + +```sh +/usr/bin/log stream --level debug \ + --predicate 'subsystem BEGINSWITH "com.thatfactory"' +``` + +Filter one artifact with an exact subsystem: + +```sh +/usr/bin/log stream --level debug \ + --predicate 'subsystem == "com.thatfactory.example"' +``` diff --git a/AgentGuidelines/Guidelines/Packages.md b/AgentGuidelines/Guidelines/Packages.md new file mode 100644 index 0000000..c33cfbb --- /dev/null +++ b/AgentGuidelines/Guidelines/Packages.md @@ -0,0 +1,173 @@ +# Swift Packages + +## README badges + +Start a new ThatFactory project or package README with a centered HTML badge block: + +```html +

+ +

+``` + +For example, a repository using all supported badge configurations could use: + +```html +

+ Swift Version + Xcode Version + Platforms + Platforms + SPM + NPM + Xcode MCP + Codex MCP + Claude MCP + DocC + License + Updated + Revision + CI + Publish + Nightly +

+``` + +Use only badges that describe the repository, in this order: + +1. Swift version. +2. Xcode version. +3. Supported platforms. +4. Relevant package manager, runtime, or ecosystem badges, such as SPM or NPM. +5. Relevant agent or tooling badges, such as Xcode MCP, Codex, or Claude. +6. DocC, documentation. +7. License. +8. Updated date. +9. Revision or latest release. +10. CI badges. +11. Release/publishing status when applicable. + +The common package baseline is Swift, Xcode, Platforms, License, and CI. Add optional badges only when they convey useful repository-specific information. Keep the order stable even when some positions are omitted. + +- Point CI, publishing, and documentation badges at workflows in the current repository; never copy another repository's badge URL unchanged. +- Use descriptive `alt` text. Preserve a repository's established Xcode badge convention when the label intentionally records the last verified Xcode version. +- Prefer dynamic Updated and Revision badges backed by repository history or releases so maintainers do not edit dates and versions by hand. +- Do not advertise a platform, integration, package manager, or agent that the repository does not support. +- Keep the repository's license in a root `LICENSE` file when reuse or redistribution is permitted. +- Retain the README license badge when the standard badge set applies, but do not add a dedicated License heading or license-description section to a package README. GitHub already presents the repository license beside the README, and the badge provides the summary without duplicating license prose. +- Do not add a license to an existing repository without the owner's explicit choice of terms. + +## Package boundaries + +- Keep a reusable package focused on one coherent capability. +- Prefer UI-agnostic domain APIs unless UI is the package's explicit purpose. +- Do not add application Redux, navigation, persistence, or product policy to a generic package. +- A first-party package must not introduce or conceal a third-party runtime dependency. Follow the [external dependency policy](Development.md#external-dependencies) before changing the dependency graph. +- Keep public APIs minimal and stable. Prefer composing focused types over introducing umbrella abstractions before multiple consumers need them. +- Declare platform and Swift toolchain requirements explicitly in `Package.swift`. +- New Swift packages must start on the latest supported Swift language and toolchain version. Before adding a major package capability to an older package, plan and complete the required Swift/toolchain modernization first. +- Put sources under `Sources//` and tests under `Tests/Tests/`. +- Keep resources in the target that owns them and use the package bundle for lookup. + +## Compiler settings baseline + +Host Xcode build settings are not a substitute for package configuration. Each package must express its applicable compiler policy in `Package.swift` so independently invoked SwiftPM builds, Xcode builds, and dependency builds receive the same strictness. This baseline is the SwiftPM representation of the applicable compiler policy in [Xcode project settings](Xcode/ProjectSettings.md), not a mechanical copy of Xcode build-setting names. + +New packages must use the newest supported Swift tools version. Every package must use the newest stable Swift language mode supported by the selected toolchain, currently Swift 6, preferably owned once at package level: + +```swift +swiftLanguageModes: [.v6] +``` + +Do not repeat `.swiftLanguageMode(.v6)` target by target when package-level ownership is sufficient. A target may specialize the language mode only under a documented package exception. An older manifest that cannot express this baseline must first modernize its tools version; `.treatAllWarnings(as:)` requires PackageDescription 6.2 or later. + +Every locally defined target that compiles Swift and for which SwiftPM exposes `swiftSettings`, including test targets and other applicable Swift target kinds, must unconditionally use the typed PackageDescription API: + +```swift +swiftSettings: [ + .treatAllWarnings(as: .error), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), +] +``` + +A setting restricted only to Debug, Release, a platform, or another build condition does not satisfy this package-wide baseline unless a documented exception covers that scope. Do not use `unsafeFlags` for warning handling when the typed API is available. + +When a locally compiled target contains C or Objective-C, require `cSettings: [.treatAllWarnings(as: .error)]`. Where C++ settings apply, require `cxxSettings: [.treatAllWarnings(as: .error)]`. Pure-Swift packages do not need C or C++ settings, and packages must not introduce `-Werror` through `unsafeFlags` when the typed APIs are available. + +Swift 6 language mode enables complete concurrency checking unconditionally, so a Swift 6 package must not retain any explicit `StrictConcurrency` opt-in. Remove `.enableUpcomingFeature("StrictConcurrency")`, `.enableExperimentalFeature("StrictConcurrency")`, and `StrictConcurrency=complete` spellings instead of treating one representation as special. For an older package, modernize to the required language mode instead of preserving the legacy mode with a compatibility flag. + +Do not invent a direct SwiftPM equivalent for `SWIFT_APPROACHABLE_CONCURRENCY`. Its applicable opt-in language behavior is represented by the individual upcoming features above, including `InferIsolatedConformances` and `NonisolatedNonsendingByDefault`. + +`.defaultIsolation(MainActor.self)` is intentionally not part of this shared package baseline. Reusable packages must express actor isolation according to their public and internal API semantics rather than inherit an application's default isolation policy. A package may choose a default isolation as an intentional package-specific architectural decision, but the completion audit must not add or require it merely for Xcode-project parity. + +Require the listed upcoming features on every applicable locally defined Swift target, but not on binary or system-library targets that SwiftPM does not compile as Swift source or package plug-in targets for which `Target.plugin(...)` does not expose `swiftSettings`. Reevaluate the list whenever the selected Xcode/Swift toolchain or language mode changes. When a feature becomes unconditional in the selected language mode, remove its `.enableUpcomingFeature(...)` declaration from packages, remove it from this baseline, and update the audit contract in the same change. Retain no redundant upcoming features merely for historical consistency because they can produce diagnostics under warnings-as-errors. + +Metal warnings-as-errors and application `Info.plist` export-compliance declarations have no package equivalent in this baseline. SwiftPM has no first-class Metal warning setting, and a reusable package does not own its consuming application's generated `Info.plist`. + +Accept a deviation only when the nearest applicable `AGENTS.md`, or durable documentation linked from it, records the exact package compiler setting or feature, affected package and targets, concrete incompatibility, replacement or omission, engineering impact, compensating validation where applicable, and condition for revisiting or removing the exception. Do not infer an exception from the existing `Package.swift`. + +## Logging + +Packages own any diagnostics emitted by their implementation. Follow the shared [logging guide](Logging.md) for AppLogger usage, subsystem identity, package emoji prefixes, domain-owned categories, concise messages, privacy, and test coverage. A consuming application must not reproduce package-internal logs. + +## Development workflow + +1. Read the package's local `AGENTS.md`, README, DocC, and public API before changing behavior. +2. Add or update tests in the package itself. +3. Update DocC and README examples when public behavior changes. +4. Run the focused tests, then `swift test` or the package's declared Xcode test workflow. +5. Integrate the package into a consumer locally only when consumer behavior must also be verified. +6. Avoid committing consumer-specific workarounds into the package when the behavior belongs in the consumer. + +## DocC documentation + +DocC is the default documentation format for public Swift packages. Document public APIs with `///` DocC comments and keep package-level conceptual material in a DocC catalog when it needs more than declaration comments. + +Before adopting the DocC command, an existing package must be updated to the latest supported Swift toolchain and declare the Swift-DocC plugin dependency in `Package.swift` (for example, `.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "")`). New packages must declare this prerequisite from the beginning when they publish DocC. + +The Swift-DocC plugin is a guideline-mandated tooling dependency under the [external dependency policy](Development.md#external-dependencies). Keep it tooling-only; do not link it into library or product runtime targets. + +Packages that publish documentation must build and deploy their DocC site as part of the release workflow: + +1. Run tests before documentation generation. +2. Generate static-hosting documentation with `swift package generate-documentation --target --disable-indexing --output-path ./public --transform-for-static-hosting --hosting-base-path `. +3. Add a root redirect to `//documentation//`. +4. Upload `./public` with `actions/upload-pages-artifact` and deploy it with `actions/deploy-pages`. +5. Grant the workflow `pages: write` and `id-token: write` permissions and expose the deployed URL in the README through a DocC badge. + +The release job must publish documentation only after the release has been approved, merged, tagged, and published. Verify the generated site locally when practical and keep the README badge URL aligned with the repository's GitHub Pages site. + +## Local integration + +- Use Xcode's local-package workflow or an explicit temporary local dependency while developing package and consumer changes together. +- Do not commit machine-specific absolute package paths. +- Before release, restore the consumer to the tagged remote dependency unless its local instructions intentionally retain a monorepo relationship. +- Verify the final remote version resolves on a clean checkout. + +## Releases + +Never release a package directly from unreviewed changes. Every release change must first be submitted through a pull request, reviewed, and approved. This rule applies to `agent-guidelines` itself as well as every consumer package. Create and publish the release only after the PR has merged. + +For ThatFactory packages, “release a new version” means: + +1. Choose a semantic version appropriate to compatibility. +2. Update public documentation and release notes. +3. Run the declared CI/test workflow. +4. Open a pull request containing the release state and wait for approval. +5. Merge the approved pull request. +6. Create and push the matching Git tag. +7. Create a GitHub release for that tag. +8. Use real multiline release notes and backticks around technical names and versions. + +When using a CLI, pass multiline notes through a file so GitHub renders line breaks correctly. + +## Consumer updates + +- Review package release notes and API changes before updating. +- Update one dependency relationship intentionally; do not rewrite unrelated resolved versions. +- Build and test the affected consumer behavior. +- Update the consumer's package integration documentation when roles, mappings, or workflows change. diff --git a/AgentGuidelines/Guidelines/Swift/Swift.md b/AgentGuidelines/Guidelines/Swift/Swift.md new file mode 100644 index 0000000..17a29f1 --- /dev/null +++ b/AgentGuidelines/Guidelines/Swift/Swift.md @@ -0,0 +1,38 @@ +# Swift + +## Language and SDK guidance + +- Use the Swift language and platform versions declared by the consumer repository. +- Use current official Apple documentation through Xcode documentation search when API behavior or availability matters. +- Use current `swift-collections` documentation when working with its collection types. +- Import the module that owns an API. For example, APIs specific to `OrderedCollections` require `import OrderedCollections`. +- Maintain a zero-warning policy for warnings introduced by the change. +- For Xcode projects, apply the shared [project-settings baseline](../Xcode/ProjectSettings.md) at project level so every application and test target inherits it. + +## Implementation + +- Prefer concise, readable, maintainable code over clever abstractions. +- Prefer structured concurrency with `async`/`await`, task groups, actors, and `Task` where appropriate. +- Do not introduce `DispatchQueue.async` as a substitute for structured concurrency. +- Respect strict concurrency and the repository's default actor isolation. +- Prefer compiler-synthesized `Codable`, `Equatable`, `Hashable`, and `Sendable` conformances when their semantics are correct. +- Write manual serialization, equality, or hashing only when a documented requirement prevents synthesis. +- Import the narrowest framework the file requires. Models should not import SwiftUI merely to gain transitive access to Foundation types. +- A new Swift file must contain at least one required import; use `import Foundation` when it otherwise needs no module. + +## State and isolation + +- Treat actor isolation as part of an API's contract. +- 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 + +When a target exposes or consumes C, Objective-C, or C++ interfaces, use Xcode's current `adopt-c-bounds-safety` skill and official compiler documentation for that scoped work. Do not apply C bounds-safety rules to pure Swift targets. + +## Documentation + +Follow [Swift style](SwiftStyle.md) for source formatting and [Documentation](../Documentation.md) for DocC and project-level guidance. diff --git a/AgentGuidelines/Guidelines/Swift/SwiftFormat.md b/AgentGuidelines/Guidelines/Swift/SwiftFormat.md new file mode 100644 index 0000000..eeb1f8d --- /dev/null +++ b/AgentGuidelines/Guidelines/Swift/SwiftFormat.md @@ -0,0 +1,108 @@ +# 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. + +## Swift package integration + +- Do not make `swift build` or `swift test` rewrite package sources. Formatting is an explicit local preparation step; builds and tests remain reproducible and non-mutating. +- Before building, testing, or handing off a package change, format and lint every checked-in Swift source root plus the manifest. A package with the standard layout runs: + + ```sh + AgentGuidelines/Scripts/swift_format.sh format-and-lint \ + Package.swift \ + Sources \ + Tests + + swift test + ``` + +- Omit a path only when it does not exist, and add nonstandard checked-in Swift source roots such as `Plugins` or `Examples`. Do not scan `.build`, generated artifacts, vendored dependencies, or another package's sources. +- Keep formatting and testing as consecutive, independently visible commands. A repository-owned convenience script may compose them, but formatting must finish before `swift test` begins and a formatting failure must stop the workflow. +- SwiftPM command plugins may provide an additional manual entry point, but they do not replace the shared configuration, wrapper, or CI check. Do not add a formatter package dependency solely to duplicate the toolchain-provided formatter without a documented repository need. + +[SwiftPM build-tool plugins](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) have read-only access to package source directories. This makes non-mutating lint possible in a custom build integration, but source-rewriting formatting does not belong inside the build. Prefer the explicit workflow above unless a package documents why every build must also pay the cost of a dedicated lint plugin. + +## CI integration + +- Run `lint-strict` in a dedicated, non-mutating job for pull requests and merges to the protected branch. Never run `format` or `format-and-lint` in CI. +- Use the same explicit source scope as the local workflow. Package CI includes `Package.swift`, `Sources`, `Tests`, and any additional checked-in Swift roots that exist. Xcode-project CI covers the union of source folders compiled by the project's independently buildable targets. +- Select the consumer's documented self-hosted macOS runner labels and supported Xcode toolchain. Keep repository-specific runner labels and Xcode selection outside this shared example. + +A typical Swift package job is: + +```yaml +swift-format: + name: Swift Format + runs-on: [self-hosted, macOS, ARM64] + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Select and log Xcode + run: | + xcodebuild -version + xcode-select -p + + - name: Run strict swift-format lint + run: | + AgentGuidelines/Scripts/swift_format.sh lint-strict \ + Package.swift \ + Sources \ + Tests +``` + +Adapt the runner labels and path list to the consumer. Keep the command shape unchanged so local execution, the consumer validator, and CI use the same shared wrapper and strict policy. + +## 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/AgentGuidelines/Guidelines/Swift/SwiftStyle.md b/AgentGuidelines/Guidelines/Swift/SwiftStyle.md new file mode 100644 index 0000000..6277537 --- /dev/null +++ b/AgentGuidelines/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/AgentGuidelines/Guidelines/Swift/SwiftUI.md new file mode 100644 index 0000000..82b7cbb --- /dev/null +++ b/AgentGuidelines/Guidelines/Swift/SwiftUI.md @@ -0,0 +1,63 @@ +# SwiftUI + +Use official Apple documentation and Xcode's current SwiftUI skills for API-specific behavior. This guide captures stable project policy rather than reproducing the current SDK's API catalog. + +## 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`. +- Avoid a single-child `Group` that adds no structure or behavior. + +## Data flow + +- Pass a view only the value-type fields it reads or forwards. +- Use private `@State` for state genuinely owned by the view. +- Use `Binding` when a child edits state owned by its parent. +- In non-Redux designs, prefer `@Observable` to `ObservableObject` for new shared reference models when platform support allows it. +- In Redux applications, keep durable app and domain state in Redux. Do not introduce an observable view model as a parallel source of truth. +- Make observable stored-property types `Equatable` when equality matches their semantics, allowing redundant assignments to avoid unnecessary invalidation. +- Isolate side effects in `.task`, `.onChange`, actions, or explicit async functions rather than hiding them in rendering logic. +- Use the current `.onChange(of:)` form and read the updated captured value when the previous value is unnecessary. +- Avoid closure-based bindings when a writable key-path binding expresses the same relationship. + +With SDKs where `@State` is a macro, do not give a state property a declaration default and then attempt to replace that value in `init`. Choose one initialization source and verify current compiler guidance when migrating existing code. + +## Collections and identity + +- Give `ForEach`, `List`, `Table`, and similar data-driven views stable, unique element identity. +- Prefer meaningful `Identifiable` conformance when the model has natural identity. +- Do not use collection indices, offsets, or transient UUIDs as identity for mutable collections. +- Do not sort, filter, or map large collections inline inside a frequently evaluated view body. Prepare the collection before rendering. +- Use a dedicated row `View` for meaningful rows and pass it narrow inputs. +- Avoid `AnyView` in collection rows. + +## Modifiers and environment + +- Preserve stable view identity. Prefer modifiers whose values change over conditionally adding and removing modifier branches. +- Do not place high-frequency values in the environment when explicit narrow inputs work. +- Avoid unstable environment defaults and freshly created closures that invalidate large subtrees. +- Do not hide unstable values behind fake `Equatable` implementations. + +## Modern APIs and scope + +- Do not introduce APIs that current Xcode documentation identifies as deprecated or soft-deprecated. +- When fixing a feature, modernize only the code directly required by the task unless the user requests a broader migration. +- For SDK-sensitive features, search current Apple documentation through Xcode rather than relying on remembered signatures. +- Apply the current Xcode SwiftUI skill when a new SDK changes source behavior, builder resolution, state initialization, or modifier availability. + +## Previews + +- Put previews at the end of the file under `// MARK: - Preview`. +- Use `#Preview`. +- Use `@Previewable` for interactive preview state when appropriate. +- Keep preview fixtures deterministic and lightweight. +- After UI changes, follow [Xcode MCP and visual verification](../Xcode/MCP.md) when runtime verification adds meaningful confidence. + +## Localization + +Follow [Localization](../Localization.md) for user-facing text, layout direction, formatting, and package bundles. diff --git a/AgentGuidelines/Guidelines/Testing/UnitTesting.md b/AgentGuidelines/Guidelines/Testing/UnitTesting.md new file mode 100644 index 0000000..a694331 --- /dev/null +++ b/AgentGuidelines/Guidelines/Testing/UnitTesting.md @@ -0,0 +1,52 @@ +# Unit and Integration Testing + +## Framework choice + +- Use Swift Testing (`import Testing`) for new unit and integration tests. +- Keep XCTest for UI automation based on XCUIAutomation and for `measure`-based performance tests. +- Do not migrate unrelated tests while implementing a focused feature or fix. +- After removing XCTest, add direct imports such as Foundation when the test relied on XCTest's transitive exports. + +## Structure + +- Organize test intent with `// Given`, `// When`, and `// Then` where the phases are meaningful. +- Prefer `struct` suites. Use a reference type only when lifecycle or identity requires it. +- Use suite initialization and `deinit` only for genuine shared setup and cleanup. +- Keep each test focused on one behavior and name it in domain language. +- Mirror production physical folders under the test target. +- Put reusable mocks and fixtures under the test target's `Mocks/` folder. +- Use shared test tags for recurring classification and keep their declarations alphabetical. Prefer small composable tags that can be combined to describe a test without repeating ad hoc metadata. +- Add a short `///` comment describing each mock's test purpose. + +## Assertions and control flow + +- Use `@Test` for tests and traits. +- Use `#expect` for assertions that allow the test to continue. +- Use `#require` for prerequisites whose failure must stop the current test. +- Prefer exact thrown-error expectations when the particular error is part of the contract. +- Use confirmation APIs for callback or delegate behavior rather than hand-built counters and arbitrary delays. +- Record known failures with Swift Testing's known-issue support instead of silently disabling coverage. + +## Concurrency + +- Assume Swift Testing may run tests concurrently and on arbitrary tasks. +- Make tests independent by default. +- Use `.serialized` only when a suite has a real shared-state dependency that cannot reasonably be removed. +- Add `@MainActor` only when the system under test requires main-actor isolation. +- Prefer async test functions and structured concurrency over expectations combined with arbitrary sleeps. +- Inject clocks, services, identifier generators, and providers to make asynchronous behavior deterministic. + +## Repetition + +- Prefer parameterized tests when the same behavior is exercised with multiple inputs and expected outputs. +- Keep argument cases readable and give complex cases a small named model. +- Do not loop manually inside one test when individual parameterized cases would produce better failure reporting. + +## Execution + +- Prefer Xcode MCP for discovering and running the relevant test plan or test target. +- Use XcodeBuildMCP only when Xcode MCP is unavailable or fails to complete the workflow. +- Start with the smallest relevant test selection, then run the broader affected suite when risk justifies it. +- Report which tests ran and whether any relevant tests could not be executed. + +Follow [Xcode MCP](../Xcode/MCP.md) for build and runtime verification. diff --git a/AgentGuidelines/Guidelines/Xcode/MCP.md b/AgentGuidelines/Guidelines/Xcode/MCP.md new file mode 100644 index 0000000..bed3e38 --- /dev/null +++ b/AgentGuidelines/Guidelines/Xcode/MCP.md @@ -0,0 +1,65 @@ +# Xcode MCP and Visual Verification + +Use Xcode as the primary source for Apple documentation, project knowledge, builds, tests, previews, simulator/device interaction, and diagnostics. + +Apple documents external agent access through [`xcrun mcpbridge`](https://developer.apple.com/documentation/xcode/giving-external-agents-access-to-xcode). Xcode must be open with the relevant project or package, and external-agent access must be enabled in Xcode settings. + +## Tool priority + +1. Use Xcode MCP for Apple documentation search and operations on an open Xcode project or package. +2. Use official Apple web documentation when Xcode documentation search is unavailable or an external reference is useful. +3. Use XcodeBuildMCP only when Xcode MCP is unavailable or cannot complete the required operation. + +Discover the tools exposed by the active Xcode server. Tool prefixes and exact names can vary by client; do not hardcode a server prefix when the active tool catalog can be inspected. + +## Documentation lookup + +- Search current Apple documentation before using an API whose signature, availability, behavior, or replacement may have changed. +- Prefer the documentation returned by the installed Xcode toolchain for SDK-sensitive work. +- Use Xcode-provided skills for specialized current workflows such as SwiftUI modernization, localization, security auditing, device interaction, and C bounds safety. +- Distill durable project policy into local documentation; do not copy an exported Apple skill into a repository. + +## Project operations + +- Prefer Xcode project-aware file, target, build-setting, issue, build, test, preview, and documentation tools when available. +- Build the smallest relevant target or scheme first. +- Read Xcode diagnostics and fix the first root failure before retrying broadly. +- Run focused tests before the full affected suite. +- Keep the project open and active throughout a multi-step Xcode MCP workflow. + +## SwiftUI previews + +After a meaningful SwiftUI change: + +1. Build the affected target. +2. Render or refresh the relevant preview when Xcode exposes preview tooling. +3. Inspect errors and warnings from the preview and build. +4. Verify representative states, localization, accessibility sizes, and appearances when relevant to the task. +5. If preview tooling is unavailable or insufficient, run the feature in a simulator or device session. + +## Build, run, and interaction + +Use runtime interaction when static compilation cannot establish that a user-visible workflow behaves correctly. + +1. Start or select an appropriate simulator/device session. +2. Build, install, and launch through Xcode tooling. +3. Prefer one-run launch arguments and environment variables to editing a shared scheme for temporary configuration. +4. Capture the accessibility or UI hierarchy before interaction. +5. Capture a screenshot when visual state matters. +6. Derive interaction targets from the hierarchy; do not guess coordinates when semantic information is available. +7. Perform the smallest interaction sequence that proves the behavior. +8. Capture the resulting hierarchy and screenshot. +9. Report both functional and visible defects. +10. End resource-heavy sessions when verification is complete. + +Retry a slow launch or interaction once when the application may still be settling. Do not add arbitrary waits as a default synchronization strategy. + +## Reporting + +State: + +- what target, scheme, preview, test, simulator, or device was used; +- what behavior was exercised; +- whether build and runtime diagnostics were clean; +- what visual evidence was inspected; +- what could not be verified and why. diff --git a/AgentGuidelines/Guidelines/Xcode/ProjectSettings.md b/AgentGuidelines/Guidelines/Xcode/ProjectSettings.md new file mode 100644 index 0000000..9d00f99 --- /dev/null +++ b/AgentGuidelines/Guidelines/Xcode/ProjectSettings.md @@ -0,0 +1,80 @@ +# Xcode Project Settings + +Use Apple's [Build settings reference](https://developer.apple.com/documentation/xcode/build-settings-reference) for the current setting names and behavior. Apply this baseline to every checked-in Xcode project. + +## Project-level ownership + +Define the required settings in every project build configuration so application, extension, framework, unit-test, UI-test, and other targets inherit one baseline. An `.xcconfig` counts as project-level ownership only when the project's build configurations reference it; a target-only configuration does not. + +Do not satisfy this policy with repeated target-level values. Remove redundant target copies so inheritance remains visible. A target may override a project value only under a documented exception that names that target and setting. + +## Required baseline + +Set all warning policies to `YES`: + +- `GCC_TREAT_WARNINGS_AS_ERRORS` +- `MTL_TREAT_WARNINGS_AS_ERRORS` +- `SWIFT_TREAT_WARNINGS_AS_ERRORS` + +Set the concurrency baseline: + +- `SWIFT_APPROACHABLE_CONCURRENCY = YES` +- `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` +- `SWIFT_STRICT_CONCURRENCY = complete` + +For application targets, set `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO` at project level so every configuration generates Apple's [`ITSAppUsesNonExemptEncryption`](https://developer.apple.com/documentation/bundleresources/information-property-list/itsappusesnonexemptencryption) Boolean with the value `NO`. This declares that the shipped app does not use non-exempt encryption and avoids App Store Connect's recurring missing-compliance questionnaire. Verify the generated Boolean in the built app before upload and reassess it whenever the app or a linked dependency introduces cryptography. + +Set `SWIFT_VERSION` to the newest stable Swift language mode supported by the selected Xcode. The current language mode is Swift 6, serialized as `SWIFT_VERSION = 6.0`; move to 7, 8, 9, and later stable modes when their supporting Xcode releases are adopted. Do not confuse the compiler's minor release, such as Swift 6.4, with the Swift language mode. + +Inspect every build setting exposed by the selected Xcode whose name begins with `SWIFT_UPCOMING_FEATURE_`. Enable each feature that remains opt-in under the selected Swift language mode. Do not set an upcoming-feature flag when that language mode already enables the feature unconditionally: Swift diagnoses some redundant flags, and warnings-as-errors can turn that diagnostic into a build failure. `SWIFT_APPROACHABLE_CONCURRENCY` enables a subset of concurrency features but does not replace the applicable explicit upcoming-feature settings. + +The Xcode 27 inventory to evaluate is: + +- `SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE` +- `SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN` +- `SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION` +- `SWIFT_UPCOMING_FEATURE_DYNAMIC_ACTOR_ISOLATION` +- `SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY` +- `SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES` +- `SWIFT_UPCOMING_FEATURE_GLOBAL_ACTOR_ISOLATED_TYPES_USABILITY` +- `SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY` +- `SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS` +- `SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS` +- `SWIFT_UPCOMING_FEATURE_INFER_ISOLATED_CONFORMANCES` +- `SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES` +- `SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT` +- `SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES` +- `SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY` +- `SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY` +- `SWIFT_UPCOMING_FEATURE_NONISOLATED_NONSENDING_BY_DEFAULT` +- `SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION` + +Treat this list as discovery input for Xcode 27, not as a set of flags that must all be present and not as a permanent exhaustive list. When adopting a newer Xcode, compare its build settings with this prefix and evaluate newly exposed settings against the selected Swift language mode. Require each still-upcoming feature at project level; omit each feature already incorporated into the language mode. + +## Swift package parity + +Whenever this Xcode project baseline adds, removes, or changes a Swift or Clang compiler policy, evaluate whether Swift Package Manager exposes a semantically equivalent setting and whether that policy is appropriate for reusable packages. When it is, update the [Swift package compiler-settings baseline](../Packages.md#compiler-settings-baseline), the Swift-package completion audit, and their structural validation in the same change. Do not mechanically copy application-only policy such as `Info.plist` declarations, Metal settings without a SwiftPM equivalent, or default MainActor isolation into the package baseline. + +## Audit procedure + +1. Identify the selected Xcode version and its newest stable Swift language mode. +2. Inspect the `PBXProject` build configurations and any project-level `.xcconfig` files. Confirm every Debug, Release, and custom configuration defines the complete baseline. +3. Compare the active Xcode's build settings with the `SWIFT_UPCOMING_FEATURE_` prefix so newly introduced settings are not missed. Use the setting documentation and compiler diagnostics for the selected language mode to distinguish still-upcoming features from features that are already unconditional. +4. Enumerate every target, including application targets and unit-test and UI-test targets, and every supported configuration. Use Xcode project-aware tooling or `xcodebuild -showBuildSettings` to verify the effective values. For each application target, also inspect the built `Info.plist` and require the `ITSAppUsesNonExemptEncryption` Boolean to be `NO`. +5. Inspect target build configurations for redundant copies, disabling values, overrides, or upcoming-feature flags that are redundant in the selected language mode. Remove redundant copies and resolve undocumented overrides. +6. Build the relevant configurations and treat every warning as a failure unless an applicable documented exception explicitly covers the setting that would otherwise promote it. + +## Exceptions + +An incompatible project or target requirement may specialize one or more settings only when the nearest applicable `AGENTS.md`, or durable project documentation linked from it, records: + +- each exact setting name; +- the affected project, configurations, and targets; +- the concrete requirement that prevents the baseline value; +- the replacement value or omitted setting and its engineering impact; +- compensating validation where relevant; and +- the condition for removing or revisiting the exception. + +The audit accepts an applicable documented exception and reports the deviation without failing the project for that setting. A transient discussion, generic statement that a project uses different settings, or the existing target configuration by itself is not an exception. + +An application that uses non-exempt encryption must set `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = YES` and document that exact setting as an exception, including the cryptography that requires the declaration and the resulting export-compliance workflow. The audit accepts a generated `ITSAppUsesNonExemptEncryption = YES` under that documented exception instead of reporting a false positive. diff --git a/AgentGuidelines/Guidelines/Xcode/Security.md b/AgentGuidelines/Guidelines/Xcode/Security.md new file mode 100644 index 0000000..5817b01 --- /dev/null +++ b/AgentGuidelines/Guidelines/Xcode/Security.md @@ -0,0 +1,35 @@ +# Xcode Security Audits + +Use this guide only for an explicitly requested security audit, hardening task, entitlement review, or a change that materially affects application security. Do not expand ordinary feature work into a repository-wide audit. + +## Source of truth + +- Use Xcode's current `audit-xcode-security-settings` skill and official Apple documentation for the detailed baseline. +- Prefer Xcode MCP project-aware tools for targets, build settings, entitlements, capabilities, privacy manifests, source searches, and file updates. +- Discover active tool names rather than hardcoding MCP server prefixes. + +## Audit scope + +Confirm the requested targets and configurations, then inspect only relevant areas: + +- deployment and compiler security settings; +- code-signing and entitlements; +- application capabilities; +- privacy manifests and required-reason APIs; +- network security configuration; +- debug-only behavior and diagnostics; +- sensitive data storage and logging; +- unsafe interoperability boundaries. + +## Changes + +- Explain the concrete risk and affected target before changing a setting or entitlement. +- Prefer Xcode-aware entitlement and project-setting tools over textual project-file manipulation. +- Preserve required capabilities and configuration-specific differences. +- Make narrow changes that can be reviewed and reverted. +- Build the affected target after project-setting changes. +- Exercise relevant runtime behavior when a change affects signing, capabilities, networking, storage, or system integration. + +## Report + +Separate findings from changes. Record the target/configuration, evidence, severity, remediation, and verification for each changed item. Do not claim a comprehensive security guarantee from a settings audit alone. diff --git a/AgentGuidelines/LICENSE b/AgentGuidelines/LICENSE new file mode 100644 index 0000000..42d8021 --- /dev/null +++ b/AgentGuidelines/LICENSE @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..cec2d20 --- /dev/null +++ b/AgentGuidelines/README.md @@ -0,0 +1,180 @@ +

+ Xcode MCP + Codex MCP + License + Updated + Revision + CI + Release +

+ +# 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 +``` + +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) +- [App Store metadata](Guidelines/AppStore.md) +- [CI/CD](Guidelines/CICD.md) +- [Development and reusability](Guidelines/Development.md) +- [Documentation](Guidelines/Documentation.md) +- [Git ignore files](Guidelines/Git/IgnoreFiles.md) +- [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) +- [GitHub pull requests](Guidelines/GitHub/PullRequests.md) +- [Localization](Guidelines/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 project settings](Guidelines/Xcode/ProjectSettings.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.33 \ + --squash +``` + +Swift consumers that adopt the shared formatter expose its configuration at the repository root so Xcode and other tools discover it: + +```sh +ln -s AgentGuidelines/Configurations/Swift/.swift-format .swift-format +ln -s AgentGuidelines/Configurations/Swift/.editorconfig .editorconfig +``` + +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. Keep the version-marked code-review contract, documentation-maintenance contract, external-dependency contract, and runtime-observability contract directly in the repository-root `AGENTS.md`; Markdown links to shared guides are navigation, not automatic instruction includes. + +Copy the shared [`.gitignore` template](Templates/.gitignore) into a new Xcode project or Swift package. Keep authored project files, workspaces, and package lockfiles eligible for version control, and follow the [Git ignore guidance](Guidelines/Git/IgnoreFiles.md) when an established consumer needs an additional project-specific rule. + +### Configure global Codex instructions + +Copy the contents of [`Templates/GlobalCodexInstructions.md`](Templates/GlobalCodexInstructions.md) into the user's global Codex instructions. + +These instructions bootstrap discovery of repository-local `AGENTS.md` files and shared guides and provide generic high-signal code-review defaults. Repository engineering policy and specialized threat models remain versioned in this repository or the consumer rather than duplicated in each user's global configuration. + +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. + +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). + +Expose the completion-audit skill at the consumer repository root so Codex can discover it: + +```sh +mkdir -p .agents/skills +ln -s ../../AgentGuidelines/.agents/skills/agent-guidelines-audit \ + .agents/skills/agent-guidelines-audit +``` + +Validate the checked-in consumer integration directly or through the completion-audit skill: + +```sh +AgentGuidelines/Scripts/validate_consumer_setup.swift +``` + +The native Swift validator checks the version-marked root Code Review, Documentation Maintenance, External Dependency, and Runtime Observability contracts, Codex subtree-review scope, `.gitattributes`, local guide links, and the audit-skill symlink. When the root `AGENTS.md` links the shared Swift-format guide, it also requires both configuration symlinks and a non-mutating `lint-strict` CI invocation. Pass `--require-swift-format` only when auditing formatter adoption before adding that guide link. + +## 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.33 \ + --squash +``` + +Confirm `AgentGuidelines/VERSION`, review the subtree diff, synchronize the marked code-review contract, documentation-maintenance contract, external-dependency contract, and runtime-observability contract when their versions change, run `AgentGuidelines/Scripts/validate_consumer_setup.swift`, 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 `Scripts/validate_guidelines.swift`. +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/prepare_localizable_symbols.swift b/AgentGuidelines/Scripts/prepare_localizable_symbols.swift new file mode 100755 index 0000000..220de6f --- /dev/null +++ b/AgentGuidelines/Scripts/prepare_localizable_symbols.swift @@ -0,0 +1,236 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +/// Marks every source-language string unit in a localization as translated. +func markStringUnitsTranslated(_ value: Any) -> Any { + if var dictionary = value as? [String: Any] { + if var stringUnit = dictionary["stringUnit"] as? [String: Any] { + stringUnit["state"] = "translated" + dictionary["stringUnit"] = stringUnit + } + for (key, child) in dictionary { + dictionary[key] = markStringUnitsTranslated(child) + } + return dictionary + } + if let array = value as? [Any] { + return array.map(markStringUnitsTranslated) + } + return value +} + +/// Returns one symbol-ready entry while preserving comments and translations. +func prepareEntry(key: String, entry: [String: Any], sourceLanguage: String) throws -> [String: Any] { + if entry["extractionState"] as? String == "stale" { + return entry + } + var localizations = entry["localizations"] as? [String: Any] ?? [:] + if let sourceLocalization = localizations[sourceLanguage] as? [String: Any] { + localizations[sourceLanguage] = markStringUnitsTranslated(sourceLocalization) + } else { + if entry["extractionState"] as? String == "manual" { + throw NSError( + domain: "SymbolPreparation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "\(key): manual entry has no \(sourceLanguage) source value" + ]) + } + localizations[sourceLanguage] = [ + "stringUnit": [ + "state": "translated", + "value": key, + ] + ] + } + var prepared = entry + prepared["extractionState"] = "manual" + prepared["localizations"] = localizations + return prepared +} + +/// Returns a catalog whose active entries generate localized Swift symbols. +func prepareCatalog(_ catalog: [String: Any]) throws -> [String: Any] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, !sourceLanguage.isEmpty else { + throw NSError( + domain: "SymbolPreparation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "catalog has no sourceLanguage" + ]) + } + guard let strings = catalog["strings"] as? [String: Any] else { + throw NSError( + domain: "SymbolPreparation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "catalog has no strings dictionary" + ]) + } + var preparedStrings: [String: Any] = [:] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any] else { + throw NSError( + domain: "SymbolPreparation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "catalog contains a non-dictionary string entry" + ]) + } + preparedStrings[key] = try prepareEntry(key: key, entry: entry, sourceLanguage: sourceLanguage) + } + var prepared = catalog + prepared["strings"] = preparedStrings + return prepared +} + +/// Returns active catalog entries that cannot generate expected Swift symbols. +func symbolIssues(_ catalog: [String: Any]) -> [String] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, + let strings = catalog["strings"] as? [String: Any] + else { + return ["catalog structure is invalid"] + } + var issues: [String] = [] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any] else { + issues.append("\(key): entry is not a dictionary") + continue + } + if entry["extractionState"] as? String == "stale" { + continue + } + if entry["extractionState"] as? String != "manual" { + issues.append("\(key): extractionState is not manual") + } + let localizations = entry["localizations"] as? [String: Any] + if localizations?[sourceLanguage] as? [String: Any] == nil { + issues.append("\(key): source localization \(sourceLanguage) is missing") + } + } + return issues +} + +/// Renders a JSON string scalar. +func renderJSONString(_ value: String) throws -> String { + let data = try JSONSerialization.data(withJSONObject: [value]) + let rendered = String(data: data, encoding: .utf8) ?? "[]" + return String(rendered.dropFirst().dropLast()) +} + +/// Renders JSON with deterministic Xcode-style spacing. +func renderJSON(_ value: Any, indentation: Int = 0) throws -> String { + if let dictionary = value as? [String: Any] { + if dictionary.isEmpty { + return "{}" + } + let keys = dictionary.keys.sorted() + var lines = ["{"] + for (index, key) in keys.enumerated() { + guard let child = dictionary[key] else { + continue + } + var rendered = try renderJSON(child, indentation: indentation + 2).components(separatedBy: "\n") + let prefix = String(repeating: " ", count: indentation + 2) + (try renderJSONString(key)) + " : " + rendered[0] = prefix + rendered[0] + if index < keys.count - 1 { + rendered[rendered.count - 1] += "," + } + lines.append(contentsOf: rendered) + } + lines.append(String(repeating: " ", count: indentation) + "}") + return lines.joined(separator: "\n") + } + if let array = value as? [Any] { + if array.isEmpty { + return "[]" + } + var lines = ["["] + for (index, child) in array.enumerated() { + var rendered = try renderJSON(child, indentation: indentation + 2).components(separatedBy: "\n") + rendered[0] = String(repeating: " ", count: indentation + 2) + rendered[0] + if index < array.count - 1 { + rendered[rendered.count - 1] += "," + } + lines.append(contentsOf: rendered) + } + lines.append(String(repeating: " ", count: indentation) + "]") + return lines.joined(separator: "\n") + } + let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed]) + guard let rendered = String(data: data, encoding: .utf8) else { + throw NSError( + domain: "SymbolPreparation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "could not render JSON value" + ]) + } + return rendered +} + +/// Returns whether two JSON objects are semantically equal. +func jsonObjectsEqual(_ lhs: Any, _ rhs: Any) -> Bool { + (lhs as AnyObject).isEqual(rhs) +} + +/// Writes text to standard error. +func writeError(_ value: String) { + FileHandle.standardError.write(Data((value + "\n").utf8)) +} + +/// Prepares explicit catalogs in place or checks whether preparation is needed. +func main() -> Int32 { + var checkOnly = false + var catalogs: [String] = [] + for argument in CommandLine.arguments.dropFirst() { + if argument == "--check" { + checkOnly = true + } else if argument == "--help" { + print("Usage: prepare_localizable_symbols.swift [--check]") + return 0 + } else if argument.hasPrefix("-") { + writeError("Unknown argument: \(argument)") + return 2 + } else { + catalogs.append(argument) + } + } + guard !catalogs.isEmpty else { + writeError("At least one String Catalog path is required.") + return 2 + } + var failed = false + for path in catalogs { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + guard let catalog = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw NSError( + domain: "SymbolPreparation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "catalog root is not a dictionary" + ]) + } + let prepared = try prepareCatalog(catalog) + if jsonObjectsEqual(prepared, catalog) { + print("\(path): already symbol-ready.") + continue + } + if checkOnly { + writeError("\(path): run this script without --check to prepare generated symbols.") + failed = true + continue + } + try (renderJSON(prepared) + "\n").write(toFile: path, atomically: true, encoding: .utf8) + let count = (prepared["strings"] as? [String: Any])?.count ?? 0 + print("\(path): prepared \(count) generated symbols.") + } catch { + writeError("\(path): \(error.localizedDescription)") + failed = true + } + } + return failed ? 1 : 0 +} + +exit(main()) diff --git a/AgentGuidelines/Scripts/swift_format.sh b/AgentGuidelines/Scripts/swift_format.sh new file mode 100755 index 0000000..7ed65b1 --- /dev/null +++ b/AgentGuidelines/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/AgentGuidelines/Scripts/validate_consumer_setup.swift b/AgentGuidelines/Scripts/validate_consumer_setup.swift new file mode 100755 index 0000000..e5b5c37 --- /dev/null +++ b/AgentGuidelines/Scripts/validate_consumer_setup.swift @@ -0,0 +1,465 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +let guidelinesRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() +let contractBegin = "" +let contractEnd = "" +let documentationContractBegin = "" +let documentationContractEnd = "" +let externalDependencyContractBegin = "" +let externalDependencyContractEnd = "" +let observabilityContractBegin = "" +let observabilityContractEnd = "" +let markdownLinkPattern = #"\[[^\]]+\]\(([^)]+)\)"# +let swiftFormatGuide = "AgentGuidelines/Guidelines/Swift/SwiftFormat.md" +let strictFormatCommandPattern = + #"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?AgentGuidelines/Scripts/swift_format\.sh\s+lint-strict(?=\s|\\|$)"# +let mutatingFormatCommandPattern = + #"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?AgentGuidelines/Scripts/swift_format\.sh\s+format(?:-and-lint)?(?=\s|\\|$)"# +let generatedAttributePattern = #"(?m)^\s*AgentGuidelines/\*\*\s+linguist-generated\s*$"# +let readmeLicenseHeadingPattern = #"(?im)^[ \t]{0,3}#{1,6}[ \t]+license[ \t]*#*[ \t]*$"# +let readmeSetextUnderlinePattern = #"^[ \t]{0,3}(?:=+|-+)[ \t]*$"# +let readmeLicenseParagraphPattern = + #"(?i)^.+?\s+is\s+available\s+under\s+the\s+.+?\s+license\.\s+See\s+\[LICENSE\]\((?:\./)?LICENSE\)\.$"# + +/// Parsed consumer-validation command-line values. +struct Arguments { + var consumerRoot: String? + var requireSwiftFormat = false +} + +/// Returns all regular-expression matches in a string. +func matches(_ pattern: String, in value: String) -> [NSTextCheckingResult] { + guard let expression = try? NSRegularExpression(pattern: pattern) else { + return [] + } + return expression.matches(in: value, range: NSRange(value.startIndex.. String? { + let range = match.range(at: index) + guard range.location != NSNotFound, let swiftRange = Range(range, in: value) else { + return nil + } + return String(value[swiftRange]) +} + +/// Reads UTF-8 text and records a labeled error on failure. +func readText(_ url: URL, errors: inout [String], label: String) -> String? { + do { + return try String(contentsOf: url, encoding: .utf8) + } catch { + errors.append("\(label): cannot read \(url.path): \(error.localizedDescription)") + return nil + } +} + +/// Extracts one uniquely marked contract block. +func extractMarkedBlock( + _ contents: String, + begin: String, + end: String, + errors: inout [String], + label: String +) -> String? { + guard contents.components(separatedBy: begin).count - 1 == 1, + contents.components(separatedBy: end).count - 1 == 1, + let start = contents.range(of: begin), + let finish = contents.range(of: end, range: start.upperBound..")) + target = target.components(separatedBy: "#").first ?? target + if target.isEmpty || ["#", "http://", "https://", "mailto:"].contains(where: target.hasPrefix) { + continue + } + let resolved = agentsURL.deletingLastPathComponent().appendingPathComponent(target).standardizedFileURL + if !FileManager.default.fileExists(atPath: resolved.path) { + errors.append("AGENTS.md: missing local link target '\(target)'") + } + } +} + +/// Returns whether root instructions link the shared Swift-format guide. +func adoptsSwiftFormat(_ contents: String) -> Bool { + for match in matches(markdownLinkPattern, in: contents) { + guard var target = capture(1, from: match, in: contents) else { + continue + } + target = target.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters( + in: CharacterSet(charactersIn: "<>")) + target = target.components(separatedBy: "#").first ?? target + if target.hasSuffix(swiftFormatGuide) { + return true + } + } + return false +} + +/// Returns complete shell invocations matching a command pattern. +func shellInvocations(_ contents: String, pattern: String) -> [String] { + let lines = contents.components(separatedBy: .newlines) + var invocations: [String] = [] + var index = 0 + while index < lines.count { + let line = lines[index] + if line.trimmingCharacters(in: .whitespaces).hasPrefix("#") || matches(pattern, in: line).isEmpty { + index += 1 + continue + } + var invocation = [line] + while invocation.last?.trimmingCharacters(in: .whitespaces).hasSuffix("\\") == true, + index + 1 < lines.count + { + index += 1 + invocation.append(lines[index]) + } + invocations.append(invocation.joined(separator: "\n")) + index += 1 + } + return invocations +} + +/// Returns direct files with one of the requested extensions. +func files(in directory: URL, extensions: Set) -> [URL] { + guard + let values = try? FileManager.default.contentsOfDirectory( + at: directory, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles] + ) + else { + return [] + } + return values.filter { extensions.contains($0.pathExtension.lowercased()) }.sorted { $0.path < $1.path } +} + +/// Returns a Markdown fence marker and run length when a line can open or close a fenced code block. +func markdownFence(in line: String) -> (character: Character, length: Int, suffix: Substring)? { + let indentation = line.prefix(while: { $0 == " " }).count + let content = line.dropFirst(indentation) + guard indentation <= 3, let character = content.first, character == "`" || character == "~" + else { + return nil + } + let length = content.prefix(while: { $0 == character }).count + guard length >= 3 else { return nil } + return (character, length, content.dropFirst(length)) +} + +/// Returns whether a line is eligible to be the title of a Setext License heading. +func isSetextLicenseTitle(_ line: String) -> Bool { + let indentation = line.prefix(while: { $0 == " " }).count + guard indentation <= 3 else { return false } + return line.dropFirst(indentation).trimmingCharacters(in: .whitespaces) + .localizedCaseInsensitiveCompare("License") == .orderedSame +} + +/// Validates package README license content without inspecting vendored or non-package documentation. +func validatePackageReadme(consumerRoot: URL, errors: inout [String]) { + guard FileManager.default.fileExists(atPath: consumerRoot.appendingPathComponent("Package.swift").path) else { + return + } + let readmeURL = consumerRoot.appendingPathComponent("README.md") + guard FileManager.default.fileExists(atPath: readmeURL.path), + let contents = readText(readmeURL, errors: &errors, label: "consumer package README") + else { + return + } + var outsideFences: [String] = [] + var activeFence: (character: Character, length: Int)? + for line in contents.components(separatedBy: .newlines) { + if let currentFence = activeFence { + if let marker = markdownFence(in: line), marker.character == currentFence.character, + marker.length >= currentFence.length, + marker.suffix.trimmingCharacters(in: .whitespaces).isEmpty + { + activeFence = nil + } + outsideFences.append("") + } else if let marker = markdownFence(in: line), + marker.character == "~" || !marker.suffix.contains("`") + { + activeFence = (marker.character, marker.length) + outsideFences.append("") + } else { + outsideFences.append(line) + } + } + let governedContents = outsideFences.joined(separator: "\n") + let lines = governedContents.components(separatedBy: .newlines) + let hasSetextLicenseHeading = lines.indices.dropLast().contains { index in + isSetextLicenseTitle(lines[index]) && !matches(readmeSetextUnderlinePattern, in: lines[index + 1]).isEmpty + } + if !matches(readmeLicenseHeadingPattern, in: governedContents).isEmpty || hasSetextLicenseHeading { + errors.append("consumer package README: README.md must not contain a dedicated License heading") + } + let paragraphs = governedContents.components(separatedBy: "\n\n").map { + $0.split(whereSeparator: \.isNewline).joined(separator: " ") + .trimmingCharacters(in: .whitespacesAndNewlines) + } + if paragraphs.contains(where: { !matches(readmeLicenseParagraphPattern, in: $0).isEmpty }) { + errors.append("consumer package README: README.md must not contain a standalone license-description paragraph") + } +} + +/// Validates non-mutating Swift-format CI integration. +func validateSwiftFormatCI(consumerRoot: URL, errors: inout [String]) { + let workflowsRoot = consumerRoot.appendingPathComponent(".github/workflows") + let workflows = files(in: workflowsRoot, extensions: ["yml", "yaml"]) + if workflows.isEmpty { + errors.append("consumer Swift format CI: no GitHub Actions workflows found under .github/workflows") + return + } + var strictWorkflows: [(URL, String, [String])] = [] + for workflow in workflows { + let relative = workflow.path.replacingOccurrences(of: consumerRoot.path + "/", with: "") + guard let contents = readText(workflow, errors: &errors, label: "consumer Swift format CI workflow \(relative)") + else { + continue + } + if !shellInvocations(contents, pattern: mutatingFormatCommandPattern).isEmpty { + errors.append( + "consumer Swift format CI: \(relative) must not mutate sources with format or format-and-lint") + } + let invocations = shellInvocations(contents, pattern: strictFormatCommandPattern) + if !invocations.isEmpty { + strictWorkflows.append((workflow, contents, invocations)) + } + } + if strictWorkflows.isEmpty { + errors.append( + "consumer Swift format CI: missing 'AgentGuidelines/Scripts/swift_format.sh lint-strict' invocation") + return + } + if !strictWorkflows.contains(where: { !matches(#"(?m)^\s*pull_request\s*:"#, in: $0.1).isEmpty }) { + errors.append("consumer Swift format CI: lint-strict does not run for pull requests") + } + let pushPattern = #"(?m)^\s*push\s*:"# + let mainPattern = #"(?m)^\s*-?\s*main\s*$|branches\s*:\s*\[[^]]*\bmain\b"# + if !strictWorkflows.contains(where: { + !matches(pushPattern, in: $0.1).isEmpty && !matches(mainPattern, in: $0.1).isEmpty + }) { + errors.append("consumer Swift format CI: lint-strict does not run for pushes to main") + } + if FileManager.default.fileExists(atPath: consumerRoot.appendingPathComponent("Package.swift").path) { + var requiredPaths = ["Package.swift"] + for name in ["Sources", "Tests"] { + var isDirectory: ObjCBool = false + if FileManager.default.fileExists( + atPath: consumerRoot.appendingPathComponent(name).path, + isDirectory: &isDirectory + ), isDirectory.boolValue { + requiredPaths.append(name) + } + } + let combined = strictWorkflows.flatMap(\.2).joined(separator: "\n") + for path in requiredPaths { + let escaped = NSRegularExpression.escapedPattern(for: path) + if matches("(? Arguments { + var arguments = Arguments() + var index = 0 + while index < values.count { + let value = values[index] + switch value { + case "--help": + print("Usage: validate_consumer_setup.swift [--consumer-root ] [--require-swift-format]") + exit(0) + case "--require-swift-format": + arguments.requireSwiftFormat = true + index += 1 + case "--consumer-root": + guard index + 1 < values.count else { + throw NSError( + domain: "ConsumerSetup", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "missing value for --consumer-root" + ]) + } + arguments.consumerRoot = values[index + 1] + index += 2 + default: + throw NSError( + domain: "ConsumerSetup", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "unknown argument: \(value)" + ]) + } + } + return arguments +} + +/// Writes text to standard error. +func writeError(_ value: String) { + FileHandle.standardError.write(Data((value + "\n").utf8)) +} + +/// Runs consumer integration validation. +func main() -> Int32 { + do { + let arguments = try parseArguments(Array(CommandLine.arguments.dropFirst())) + let root: URL + if let consumerRoot = arguments.consumerRoot { + root = URL(fileURLWithPath: consumerRoot) + } else { + guard guidelinesRoot.lastPathComponent == "AgentGuidelines" else { + print( + "Consumer setup validation failed:\n" + + "- --consumer-root is required when this checkout is not installed as an AgentGuidelines subtree" + ) + return 1 + } + root = guidelinesRoot.deletingLastPathComponent() + } + var errors: [String] = [] + validateConsumerSetup( + errors: &errors, + consumerRoot: root, + requireSwiftFormat: arguments.requireSwiftFormat + ) + if !errors.isEmpty { + print("Consumer setup validation failed:") + for error in errors { print("- \(error)") } + return 1 + } + let version = try String( + contentsOf: guidelinesRoot.appendingPathComponent("VERSION"), + encoding: .utf8 + ).trimmingCharacters(in: .whitespacesAndNewlines) + print("Validated consumer setup for agent-guidelines \(version).") + return 0 + } catch { + writeError("Consumer setup validation failed: \(error.localizedDescription)") + return 2 + } +} + +exit(main()) diff --git a/AgentGuidelines/Scripts/validate_guidelines.swift b/AgentGuidelines/Scripts/validate_guidelines.swift new file mode 100755 index 0000000..39b58d6 --- /dev/null +++ b/AgentGuidelines/Scripts/validate_guidelines.swift @@ -0,0 +1,830 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() +let readme = root.appendingPathComponent("README.md") +let versionFile = root.appendingPathComponent("VERSION") +let changelog = root.appendingPathComponent("CHANGELOG.md") +let swiftFormatConfiguration = root.appendingPathComponent("Configurations/Swift/.swift-format") +let editorConfiguration = root.appendingPathComponent("Configurations/Swift/.editorconfig") +let swiftFormatScript = root.appendingPathComponent("Scripts/swift_format.sh") +let swiftFormatGuideline = root.appendingPathComponent("Guidelines/Swift/SwiftFormat.md") +let localizationGuideline = root.appendingPathComponent("Guidelines/Localization.md") +let appStoreGuideline = root.appendingPathComponent("Guidelines/AppStore.md") +let xcodeProjectSettingsGuideline = root.appendingPathComponent("Guidelines/Xcode/ProjectSettings.md") +let localizationPreparationScript = root.appendingPathComponent("Scripts/prepare_localizable_symbols.swift") +let localizationValidationScript = root.appendingPathComponent("Scripts/validate_string_catalogs.swift") +let consumerSetupScript = root.appendingPathComponent("Scripts/validate_consumer_setup.swift") +let testRunner = root.appendingPathComponent("Tests/run_tests.swift") +let auditSkill = root.appendingPathComponent(".agents/skills/agent-guidelines-audit/SKILL.md") +let markdownWrappingScript = root.appendingPathComponent( + ".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift" +) +let stringCatalogInspectionScript = root.appendingPathComponent( + ".agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift" +) +let developmentGuideline = root.appendingPathComponent("Guidelines/Development.md") +let cicdGuideline = root.appendingPathComponent("Guidelines/CICD.md") +let documentationGuideline = root.appendingPathComponent("Guidelines/Documentation.md") +let packagesGuideline = root.appendingPathComponent("Guidelines/Packages.md") +let agentsTemplate = root.appendingPathComponent("Templates/AGENTS.md") +let gitignoreTemplate = root.appendingPathComponent("Templates/.gitignore") +let gitignoreGuideline = root.appendingPathComponent("Guidelines/Git/IgnoreFiles.md") + +let expectedSwiftFormatRules: [String: Bool] = [ + "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, +] + +let markdownLinkPattern = #"\[[^\]]+\]\(([^)]+)\)"# +let semanticVersionPattern = + #"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"# +let forbiddenContent = [ + "/" + "Users" + "/": "personal absolute path", + "file" + "://": "local file URL", + "mobile-ios-" + "chauffeur": "work-repository identifier", + "black" + "lane": "work-repository identifier", +] +let upcomingFeatureSettings = [ + "SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE", + "SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN", + "SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION", + "SWIFT_UPCOMING_FEATURE_DYNAMIC_ACTOR_ISOLATION", + "SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY", + "SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES", + "SWIFT_UPCOMING_FEATURE_GLOBAL_ACTOR_ISOLATED_TYPES_USABILITY", + "SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY", + "SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS", + "SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS", + "SWIFT_UPCOMING_FEATURE_INFER_ISOLATED_CONFORMANCES", + "SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES", + "SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT", + "SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES", + "SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY", + "SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY", + "SWIFT_UPCOMING_FEATURE_NONISOLATED_NONSENDING_BY_DEFAULT", + "SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION", +] +let packageUpcomingFeatures = [ + "ExistentialAny", + "InferIsolatedConformances", + "InternalImportsByDefault", + "MemberImportVisibility", + "NonisolatedNonsendingByDefault", +] + +/// Returns all regular-expression matches in a string. +func matches(_ pattern: String, in value: String) -> [NSTextCheckingResult] { + guard let expression = try? NSRegularExpression(pattern: pattern) else { + return [] + } + return expression.matches(in: value, range: NSRange(value.startIndex.. String? { + let range = match.range(at: index) + guard range.location != NSNotFound, let swiftRange = Range(range, in: value) else { + return nil + } + return String(value[swiftRange]) +} + +/// Returns a repository-relative path. +func relativePath(_ url: URL) -> String { + url.standardizedFileURL.path.replacingOccurrences(of: root.standardizedFileURL.path + "/", with: "") +} + +/// Reads UTF-8 text or records an error. +func readText(_ url: URL, errors: inout [String]) -> String? { + do { + return try String(contentsOf: url, encoding: .utf8) + } catch { + errors.append("\(relativePath(url)): cannot read file: \(error.localizedDescription)") + return nil + } +} + +/// Recursively discovers regular files while excluding repository internals and build caches. +func recursiveFiles(below directory: URL) -> [URL] { + guard + let enumerator = FileManager.default.enumerator( + at: directory, + includingPropertiesForKeys: [.isRegularFileKey], + options: [] + ) + else { + return [] + } + var files: [URL] = [] + for case let url as URL in enumerator { + let relative = relativePath(url) + if relative.split(separator: "/").contains(where: { $0 == ".git" || $0 == ".build" || $0 == "__pycache__" }) { + if (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true { + enumerator.skipDescendants() + } + continue + } + if (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true { + files.append(url) + } + } + return files.sorted { $0.path < $1.path } +} + +/// Returns all public-text files governed by the privacy scan. +func textFiles() -> [URL] { + let suffixes: Set = ["md", "sh", "swift", "txt", "yml", "yaml"] + var files = recursiveFiles(below: root).filter { suffixes.contains($0.pathExtension.lowercased()) } + for url in [versionFile, root.appendingPathComponent("LICENSE"), swiftFormatConfiguration, editorConfiguration] + where FileManager.default.fileExists(atPath: url.path) { + files.append(url) + } + return Array(Set(files)).sorted { $0.path < $1.path } +} + +/// Resolves a relative Markdown link target within the source repository. +func resolveLink(source: URL, rawTarget: String) -> URL? { + var target = rawTarget.trimmingCharacters(in: .whitespacesAndNewlines) + .trimmingCharacters(in: CharacterSet(charactersIn: "<>")) + target = target.components(separatedBy: "#").first ?? target + if target.isEmpty || ["#", "http://", "https://", "mailto:"].contains(where: target.hasPrefix) { + return nil + } + let parts = NSString(string: target).pathComponents + if let index = parts.firstIndex(of: "AgentGuidelines") { + let suffix = parts.dropFirst(index + 1).joined(separator: "/") + return root.appendingPathComponent(suffix).standardizedFileURL + } + return source.deletingLastPathComponent().appendingPathComponent(target).standardizedFileURL +} + +/// Validates all relative Markdown link targets. +func validateLinks(_ errors: inout [String]) { + for source in recursiveFiles(below: root).filter({ $0.pathExtension.lowercased() == "md" }) { + guard let contents = readText(source, errors: &errors) else { + continue + } + for match in matches(markdownLinkPattern, in: contents) { + guard let rawTarget = capture(1, from: match, in: contents), + let resolved = resolveLink(source: source, rawTarget: rawTarget) + else { + continue + } + if !FileManager.default.fileExists(atPath: resolved.path) { + errors.append("\(relativePath(source)): missing link target '\(rawTarget)'") + } + } + } +} + +/// Validates that the README catalogs every shared guide. +func validateCatalog(_ errors: inout [String]) { + guard let contents = readText(readme, errors: &errors) else { + return + } + let guideRoot = root.appendingPathComponent("Guidelines") + for guide in recursiveFiles(below: guideRoot).filter({ $0.pathExtension.lowercased() == "md" }) { + let relative = relativePath(guide) + if !contents.contains("](\(relative))") { + errors.append("README.md: guideline is not cataloged: \(relative)") + } + } +} + +/// Validates the semantic version and matching changelog heading. +func validateVersion(_ errors: inout [String]) { + guard let version = readText(versionFile, errors: &errors)?.trimmingCharacters(in: .whitespacesAndNewlines) else { + return + } + if matches(semanticVersionPattern, in: version).isEmpty { + errors.append("VERSION: invalid semantic version '\(version)'") + } + if let contents = readText(changelog, errors: &errors), !contents.contains("## [\(version)]") { + errors.append("CHANGELOG.md: missing release heading for \(version)") + } +} + +/// Validates required README installation and integration contracts. +func validateReadmeContract(_ errors: inout [String]) { + guard let contents = readText(readme, errors: &errors) else { + return + } + if let version = readText(versionFile, errors: &errors)?.trimmingCharacters(in: .whitespacesAndNewlines), + contents.components(separatedBy: version).count - 1 != 2 + { + errors.append("README.md: installation and consumer-update commands must both use VERSION \(version)") + } + let required = [ + "alt=\"Xcode MCP\"": "Xcode MCP 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", + "validate_consumer_setup.swift": "consumer setup validation command", + "--require-swift-format": "explicit Swift-format adoption validation", + "documentation-maintenance contract": "documentation contract synchronization", + "external-dependency contract": "external dependency contract synchronization", + "runtime-observability contract": "runtime observability contract synchronization", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("README.md: missing \(description): '\(value)'") + } +} + +/// Validates that public files contain no private or consumer-specific content. +func validatePublicContent(_ errors: inout [String]) { + for url in textFiles() { + guard let contents = readText(url, errors: &errors) else { + continue + } + for (forbidden, description) in forbiddenContent + where contents.localizedCaseInsensitiveContains(forbidden) { + errors.append("\(relativePath(url)): contains \(description): '\(forbidden)'") + } + } +} + +/// Returns whether two Foundation JSON values are equal. +func jsonEqual(_ lhs: Any?, _ rhs: Any?) -> Bool { + guard let lhs, let rhs else { + return lhs == nil && rhs == nil + } + return (lhs as AnyObject).isEqual(rhs) +} + +/// Validates the shared swift-format configuration. +func validateSwiftFormatConfiguration(_ errors: inout [String]) { + let configuration: [String: Any] + do { + let data = try Data(contentsOf: swiftFormatConfiguration) + guard let parsed = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw NSError( + domain: "GuidelineValidation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "root value is not an object" + ]) + } + configuration = parsed + } catch { + errors.append("Configurations/Swift/.swift-format: invalid JSON: \(error.localizedDescription)") + return + } + let expectedValues: [String: Any] = [ + "indentation": ["spaces": 4], + "indentSwitchCaseLabels": false, + "lineLength": 120, + "tabWidth": 4, + "version": 1, + ] + for (key, expected) in expectedValues where !jsonEqual(configuration[key], expected) { + errors.append( + "Configurations/Swift/.swift-format: \(key) must be \(expected), found \(String(describing: configuration[key]))" + ) + } + let orderedImports = configuration["orderedImports"] as? [String: Any] + if orderedImports?["includeConditionalImports"] as? Bool != true { + errors.append( + "Configurations/Swift/.swift-format: orderedImports.includeConditionalImports must be true, found " + + String(describing: orderedImports?["includeConditionalImports"]) + ) + } + guard let rules = configuration["rules"] as? [String: Any], !rules.isEmpty else { + errors.append("Configurations/Swift/.swift-format: rules must be an exhaustive non-empty object") + return + } + let expectedKeys = Set(expectedSwiftFormatRules.keys) + let actualKeys = Set(rules.keys) + let missing = expectedKeys.subtracting(actualKeys).sorted() + let unexpected = actualKeys.subtracting(expectedKeys).sorted() + if !missing.isEmpty || !unexpected.isEmpty { + errors.append( + "Configurations/Swift/.swift-format: rule map mismatch; missing=\(missing), unexpected=\(unexpected)") + } + for rule in expectedKeys.intersection(actualKeys).sorted() { + let expected = expectedSwiftFormatRules[rule] ?? false + if rules[rule] as? Bool != expected { + errors.append( + "Configurations/Swift/.swift-format: \(rule) must be " + + "\(expected), found \(String(describing: rules[rule]))" + ) + } + } +} + +/// Validates the shared EditorConfig values. +func validateEditorConfiguration(_ errors: inout [String]) { + guard let contents = readText(editorConfiguration, errors: &errors) else { + return + } + let 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 required.sorted() where !contents.contains(value) { + errors.append("Configurations/Swift/.editorconfig: missing '\(value)'") + } +} + +/// Validates that an expected executable file exists. +func validateExecutable(_ url: URL, description: String, errors: inout [String]) { + guard FileManager.default.fileExists(atPath: url.path) else { + errors.append("\(relativePath(url)): missing \(description)") + return + } + if !FileManager.default.isExecutableFile(atPath: url.path) { + errors.append("\(relativePath(url)): \(description) is not executable") + } +} + +/// Validates native Swift ownership for repository automation. +func validateScriptLanguages(_ errors: inout [String]) { + let scriptsRoot = root.appendingPathComponent("Scripts") + let allowedShellPath = "Scripts/swift_format.sh" + for url in recursiveFiles(below: scriptsRoot) { + let relative = relativePath(url) + if url.pathExtension == "swift" || relative == allowedShellPath { + continue + } + errors.append("\(relative): repository scripts must use Swift; only \(allowedShellPath) is retained") + } + let auditScriptsRoot = root.appendingPathComponent(".agents/skills/agent-guidelines-audit/scripts") + for url in recursiveFiles(below: auditScriptsRoot) where url.pathExtension != "swift" { + errors.append("\(relativePath(url)): audit helper scripts must use Swift") + } + let testsRoot = root.appendingPathComponent("Tests") + for url in recursiveFiles(below: testsRoot) where url.pathExtension != "swift" { + errors.append("\(relativePath(url)): repository test automation must use Swift") + } + validateExecutable(testRunner, description: "native Swift test runner", errors: &errors) + for workflowPath in [".github/workflows/ci.yml", ".github/workflows/release.yml"] { + let workflow = root.appendingPathComponent(workflowPath) + guard let contents = readText(workflow, errors: &errors) else { continue } + for required in ["Tests/run_tests.swift", "Scripts/validate_guidelines.swift"] + where !contents.contains(required) { + errors.append("\(workflowPath): missing native validation command '\(required)'") + } + if contents.localizedCaseInsensitiveContains("python") { + errors.append("\(workflowPath): repository validation must not require Python") + } + } +} + +/// Validates the shared Swift-format workflow guide. +func validateSwiftFormatGuideline(_ errors: inout [String]) { + guard let contents = readText(swiftFormatGuideline, errors: &errors) else { + return + } + let required = [ + "## Swift package integration": "Swift package workflow", + "format-and-lint \\": "local package formatting command", + "Package.swift": "package manifest formatting scope", + "## CI integration": "CI workflow", + "lint-strict \\": "strict CI command", + "Never run `format` or `format-and-lint` in CI": "non-mutating CI rule", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Swift/SwiftFormat.md: missing \(description): '\(value)'") + } +} + +/// Validates the documentation-maintenance policy. +func validateDocumentationGuideline(_ errors: inout [String]) { + guard let contents = readText(documentationGuideline, errors: &errors) else { + return + } + let required = [ + "Documentation is part of implementation": "implementation-time documentation rule", + "Regardless of change size": "existing-document staleness rule", + "inaccurate, incomplete, misleading, or obsolete": "stale documentation criteria", + "A small change that leaves durable knowledge and existing documentation accurate": + "minor-change documentation churn guardrail", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Documentation.md: missing \(description): '\(value)'") + } +} + +/// Validates the generated-symbol localization workflow. +func validateLocalizationGuideline(_ errors: inout [String]) { + guard let contents = readText(localizationGuideline, errors: &errors) else { + return + } + let required = [ + "using-generated-localizable-symbols-in-your-code": "Apple generated-symbol reference", + "localizing-your-app-using-agents": "Apple agent-localization reference", + "Xcode-generated `LocalizedStringResource` symbols": "generated-symbol default", + "prepare_localizable_symbols.swift": "shared symbol-preparation workflow", + "validate_string_catalogs.swift": "shared catalog-validation workflow", + "stale extracted entry": "stale-entry policy", + "marked `new` or `needs_review`": "translation-state policy", + "placeholder positions, semantic names, and conversion types": "format-signature policy", + "product voice, terminology": "consumer-specific translation boundary", + "small repository-owned wrapper": "consumer configuration boundary", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Localization.md: missing \(description): '\(value)'") + } +} + +/// Validates native reusable localization scripts. +func validateLocalizationScripts(_ errors: inout [String]) { + let scripts: [(URL, [String])] = [ + (localizationPreparationScript, ["prepareCatalog", "symbolIssues", "--check"]), + ( + localizationValidationScript, + [ + "--catalog-directory", "--source-directory", "--required-language", "formatSignatureIssues", + "translationStateIssues", "literalLocalizationReferences", + ] + ), + ] + for (script, requiredValues) in scripts { + validateExecutable(script, description: "localization script", errors: &errors) + guard let contents = readText(script, errors: &errors) else { + continue + } + if contents.contains("Headroom") { + errors.append("\(relativePath(script)): contains consumer-specific logic") + } + for value in requiredValues where !contents.contains(value) { + errors.append("\(relativePath(script)): missing localization behavior '\(value)'") + } + } +} + +/// Validates the Xcode project-settings contract. +func validateXcodeProjectSettingsGuideline(_ errors: inout [String]) { + guard let contents = readText(xcodeProjectSettingsGuideline, errors: &errors) else { + return + } + let required = [ + "https://developer.apple.com/documentation/xcode/build-settings-reference": + "official Apple build-settings reference", + "GCC_TREAT_WARNINGS_AS_ERRORS": "C and Objective-C warning policy", + "MTL_TREAT_WARNINGS_AS_ERRORS": "Metal warning policy", + "SWIFT_TREAT_WARNINGS_AS_ERRORS": "Swift warning policy", + "SWIFT_APPROACHABLE_CONCURRENCY = YES": "approachable concurrency baseline", + "SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor": "default actor isolation baseline", + "SWIFT_STRICT_CONCURRENCY = complete": "strict concurrency baseline", + "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO": "export-compliance build-setting baseline", + "ITSAppUsesNonExemptEncryption`](https://developer.apple.com/": "official export-compliance key reference", + "built `Info.plist`": "built export-compliance verification", + "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = YES": "documented non-exempt encryption exception", + "newest stable Swift language mode": "future-facing Swift language policy", + "Enable each feature that remains opt-in": "language-mode-aware upcoming-feature policy", + "warnings-as-errors can turn that diagnostic into a build failure": "redundant upcoming-feature safety rule", + "Xcode 27 inventory to evaluate": "Xcode 27 upcoming-feature inventory", + "project-level `.xcconfig`": "project-level configuration ownership", + "unit-test and UI-test targets": "test-target effective-value audit", + "nearest applicable `AGENTS.md`": "local exception source", + "condition for removing or revisiting the exception": "exception lifecycle", + "## Swift package parity": "Swift package parity section", + "../Packages.md#compiler-settings-baseline": "Swift package baseline cross-reference", + "Whenever this Xcode project baseline adds, removes, or changes a Swift or Clang compiler policy": + "package applicability maintenance rule", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Xcode/ProjectSettings.md: missing \(description): '\(value)'") + } + for setting in upcomingFeatureSettings where !contents.contains(setting) { + errors.append( + "Guidelines/Xcode/ProjectSettings.md: missing Xcode 27 upcoming-feature inventory entry: '\(setting)'") + } +} + +/// Validates the Swift package compiler-settings contract. +func validatePackageCompilerSettingsGuideline(_ errors: inout [String]) { + guard let contents = readText(packagesGuideline, errors: &errors) else { + return + } + let required = [ + "## Compiler settings baseline": "compiler-settings baseline section", + "Xcode/ProjectSettings.md": "Xcode baseline cross-reference", + "swiftLanguageModes": "package-level Swift language mode", + ".treatAllWarnings(as: .error)": "typed warnings-as-errors policy", + "Swift 6 language mode enables complete concurrency checking unconditionally": + "Swift 6 strict-concurrency explanation", + ".enableUpcomingFeature(\"StrictConcurrency\")": + "upcoming StrictConcurrency redundancy rule", + ".enableExperimentalFeature(\"StrictConcurrency\")": + "experimental StrictConcurrency redundancy rule", + "StrictConcurrency=complete": "explicit StrictConcurrency redundancy rule", + "`.defaultIsolation(MainActor.self)` is intentionally not part": + "default MainActor isolation exclusion", + "package plug-in targets for which `Target.plugin(...)` does not expose `swiftSettings`": + "unsupported plug-in target exclusion", + "Retain no redundant upcoming features": "redundant upcoming-feature prohibition", + "condition for revisiting or removing the exception": "package-setting exception lifecycle", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Packages.md: missing package compiler policy \(description): '\(value)'") + } + for feature in packageUpcomingFeatures where !contents.contains(".enableUpcomingFeature(\"\(feature)\")") { + errors.append("Guidelines/Packages.md: missing required SwiftPM upcoming feature '\(feature)'") + } +} + +/// Validates the shared App Store metadata workflow. +func validateAppStoreGuideline(_ errors: inout [String]) { + guard let contents = readText(appStoreGuideline, errors: &errors) else { + return + } + let required = [ + "app-store-connect-mcp": "MCP source", + "AppStore/": "repository metadata root", + "validate_repository": "repository validation", + "plan_metadata_changes": "metadata planning", + "plan_screenshot_changes": "screenshot planning", + "apply_plan": "immutable plan application", + "get_operation_status": "uncertain-outcome recovery", + "noOp: true": "remote reconciliation", + ".appstore-connect-mcp/": "ignored operational state", + "does not authorize submitting": "submission boundary", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/AppStore.md: missing \(description): '\(value)'") + } +} + +/// Validates the native-first dependency policy. +func validateExternalDependencyPolicy(_ errors: inout [String]) { + if let contents = readText(developmentGuideline, errors: &errors) { + let required = [ + "## External dependencies": "external dependency policy section", + "Do not introduce a new third-party source or binary dependency": "native and first-party default", + "explicit approval from the repository owner": "repository-owner approval gate", + "durable repository documentation": "durable exception record", + "Tooling dependencies explicitly required by these shared guidelines": "tooling-only exception", + "CocoaPods and Carthage are forbidden": "forbidden package managers", + "Use Swift Package Manager for package dependencies": "Swift Package Manager requirement", + "## Post-merge cleanup": "post-merge cleanup section", + "fast-forward-only pull": "safe primary-branch update", + "delete the merged local feature branch": "merged local branch removal", + "Confirm the remote feature branch is absent": "remote branch cleanup verification", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Development.md: missing \(description): '\(value)'") + } + } + if let contents = readText(cicdGuideline, errors: &errors) { + let required = [ + "## Tooling and automation": "CI/CD tooling policy section", + "## Private repository dependencies": "private repository dependency authentication section", + "Fastlane is forbidden": "forbidden delivery tooling", + "xcode-cloud-mcp": "first-party Xcode Cloud tooling", + "app-store-connect-mcp": "first-party App Store tooling", + "required behavior cannot be implemented": "non-Swift capability-gap threshold", + "missing Swift capability": "documented non-Swift exception", + "actions/create-github-app-token@v3": "short-lived GitHub App token workflow", + "client-id:": "current GitHub App client identifier input", + "permission-contents: read": "read-only private dependency permission", + "GIT_CONFIG_KEY_0": "process-level Git authentication", + "GIT_CONFIG_VALUE_0: https://github.com/": "GitHub HTTPS rewrite source", + "complete subprocess tree as privileged": "credential-bearing subprocess trust boundary", + "isolated disposable or ephemeral self-hosted runner": "untrusted-code runner isolation", + "This trust rule is event-independent": "event-independent credential boundary", + "pull_request_target": "untrusted pull-request credential boundary", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/CICD.md: missing \(description): '\(value)'") + } + } + if let contents = readText(packagesGuideline, errors: &errors) { + let required = [ + "[external dependency policy](Development.md#external-dependencies)": "package dependency policy pointer", + "must not introduce or conceal a third-party runtime dependency": "first-party package boundary", + "guideline-mandated tooling dependency": "DocC tooling exception", + "root `LICENSE` file": "root license-file retention", + "Retain the README license badge": "README license-badge retention", + "do not add a dedicated License heading or license-description section": + "README license-section prohibition", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Packages.md: missing \(description): '\(value)'") + } + } + if let contents = readText(agentsTemplate, errors: &errors), + !contents.contains("BEGIN THATFACTORY EXTERNAL DEPENDENCY CONTRACT v1") + { + errors.append("Templates/AGENTS.md: missing external-dependency contract") + } +} + +/// Validates the completion-audit skill and native helper scripts. +func validateAuditSkill(_ errors: inout [String]) { + guard let contents = readText(auditSkill, errors: &errors) else { + return + } + let required = [ + "name: agent-guidelines-audit": "skill name", + "before claiming completion": "completion trigger", + "git diff --check": "diff validation", + "validate_consumer_setup.swift": "consumer integration validation", + "format-and-lint": "local Swift-format audit", + "lint-strict": "strict Swift-format CI audit", + "AppLogger": "AppLogger integration audit", + "Logging.md": "shared Logging guide reference", + "Dependency declaration and target linkage alone": "lifecycle observability coverage audit", + "## Audit documentation consistency": "documentation drift audit", + "Known stale documentation blocks completion": "stale documentation stopping rule", + "## Audit documentation formatting": "documentation formatting audit", + "check_markdown_wrapping.swift": "Markdown line-wrapping check", + "existing root Markdown files and declared durable documentation folders": + "documentation-convention adoption pass", + "new third-party dependency": "third-party dependency audit", + "repository-owner approval": "third-party dependency approval gate", + "no unresolved P0/P1 blocker remains": "Codex review stopping rule", + "## Audit Xcode project settings": "Xcode project-settings audit", + "Xcode/ProjectSettings.md": "shared Xcode project-settings guide reference", + "GCC_TREAT_WARNINGS_AS_ERRORS": "warnings-as-errors project audit", + "SWIFT_UPCOMING_FEATURE_": "future-facing upcoming-feature audit", + "SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor": "actor-isolation project audit", + "SWIFT_VERSION": "Swift language-version project audit", + "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO": "export-compliance project audit", + "## Audit App Store metadata": "App Store metadata audit", + "noOp: true": "App Store remote reconciliation audit", + "xcodebuild -showBuildSettings": "effective target build-setting inspection", + "nearest applicable `AGENTS.md`": "project-setting exception lookup", + "## Audit localization": "localization audit", + "Guidelines/Localization.md": "shared Localization guide reference", + "prepare_localizable_symbols.swift": "generated-symbol preparation audit", + "validate_string_catalogs.swift": "String Catalog validation audit", + "check_xcstrings_inspection.swift": "String Catalog editor evidence gate", + "Fail closed when a changed catalog lacks that recorded editor evidence": + "missing String Catalog editor evidence stopping rule", + "pull-request description": "durable pull-request evidence summary", + "A local wrapper may": "consumer localization-wrapper boundary", + "new repository-owned executable scripts": "native Swift script audit", + "Templates/.gitignore": "shared gitignore template comparison", + "# Project-specific: ": "project-specific gitignore exception marker", + "git check-ignore -v": "gitignore behavior verification", + "git ls-files": "tracked-file safety check", + "CocoaPods or Carthage": "forbidden package-manager audit", + "Guidelines/CICD.md": "shared CI/CD guide reference", + "fastlane adoption": "forbidden delivery-tooling audit", + "missing Swift capability": "non-Swift script exception audit", + "## Audit Swift package settings": "Swift package-settings audit", + "Guidelines/Packages.md#compiler-settings-baseline": "shared package compiler-settings guide reference", + "Package.swift": "package manifest discovery", + "swift package --package-path dump-package": "evaluated manifest inspection", + "swiftLanguageModes": "package language-mode audit", + ".treatAllWarnings(as: .error)": "package warnings-as-errors audit", + ".enableUpcomingFeature(...)": "package upcoming-feature audit", + "complete strict concurrency as supplied by the language mode": "Swift 6 strict-concurrency handling", + ".enableUpcomingFeature(\"StrictConcurrency\")": "upcoming StrictConcurrency removal audit", + ".enableExperimentalFeature(\"StrictConcurrency\")": "experimental StrictConcurrency removal audit", + "StrictConcurrency=complete": "explicit StrictConcurrency removal audit", + ".defaultIsolation(MainActor.self)": "default MainActor isolation exclusion", + "package plug-in targets": "unsupported plug-in target exclusion", + "package-setting failure": "package-setting exception lookup", + ] + for (value, description) in required where !contents.contains(value) { + errors.append(".agents/skills/agent-guidelines-audit/SKILL.md: missing \(description): '\(value)'") + } + for feature in packageUpcomingFeatures where !contents.contains(feature) { + errors.append(".agents/skills/agent-guidelines-audit/SKILL.md: missing package feature audit '\(feature)'") + } + validateExecutable(markdownWrappingScript, description: "Markdown wrapping checker", errors: &errors) + validateExecutable(stringCatalogInspectionScript, description: "String Catalog inspection checker", errors: &errors) + if let development = readText(developmentGuideline, errors: &errors), + !development.contains("$agent-guidelines-audit") + { + errors.append("Guidelines/Development.md: missing mandatory $agent-guidelines-audit invocation") + } + if let template = readText(agentsTemplate, errors: &errors) { + let requiredTemplateValues = [ + "AgentGuidelines/Guidelines/Development.md": "Development.md pointer", + "BEGIN THATFACTORY DOCUMENTATION MAINTENANCE CONTRACT v1": "documentation-maintenance contract", + "BEGIN THATFACTORY EXTERNAL DEPENDENCY CONTRACT v1": "external-dependency contract", + "BEGIN THATFACTORY RUNTIME OBSERVABILITY CONTRACT v1": "runtime-observability contract", + "AgentGuidelines/Guidelines/Documentation.md": "Documentation.md pointer", + "## Stack": "Stack section", + ] + for (value, description) in requiredTemplateValues where !template.contains(value) { + errors.append("Templates/AGENTS.md: missing \(description)") + } + } +} + +/// Validates the reusable ignore template and its shared reconciliation policy. +func validateGitignoreGuidance(_ errors: inout [String]) { + guard let template = readText(gitignoreTemplate, errors: &errors) else { return } + let activePatterns = template.split(separator: "\n").map(String.init) + let requiredPatterns = [ + ".DS_Store", "xcuserdata/", "/build/", "/DerivedData/", "/.build/", "/Packages/", + ".swiftpm/configuration/registries.json", "node_modules/", "dist/", "coverage/", "__pycache__/", + ".venv/", ".env", "!.env.example", ".netrc", ".appstore-connect-mcp/", ".devspace/", + "/public-check/", + ] + for pattern in requiredPatterns where !activePatterns.contains(pattern) { + errors.append("Templates/.gitignore: missing required pattern '\(pattern)'") + } + for forbidden in ["Package.resolved", "*.xcodeproj", "*.xcworkspace", ".swiftpm/"] + where activePatterns.contains(forbidden) { + errors.append("Templates/.gitignore: must not ignore authored or lockfile path '\(forbidden)'") + } + for forbiddenPrefix in ["Carthage/", "fastlane/", "Pods/"] + where activePatterns.contains(where: { $0.hasPrefix(forbiddenPrefix) }) { + errors.append("Templates/.gitignore: must not include forbidden tooling pattern '\(forbiddenPrefix)'") + } + guard let guideline = readText(gitignoreGuideline, errors: &errors) else { return } + for required in [ + "active `.gitignore` patterns", "# Project-specific: ", "git check-ignore -v", "git ls-files", + "Do not remove it or silently accept it", + ] where !guideline.contains(required) { + errors.append("Guidelines/Git/IgnoreFiles.md: missing ignore reconciliation policy '\(required)'") + } +} + +/// Runs every repository guideline validation. +func main() -> Int32 { + var errors: [String] = [] + validateLinks(&errors) + validateCatalog(&errors) + validateVersion(&errors) + validateReadmeContract(&errors) + validatePublicContent(&errors) + validateSwiftFormatConfiguration(&errors) + validateEditorConfiguration(&errors) + validateExecutable(swiftFormatScript, description: "Swift-format script", errors: &errors) + validateScriptLanguages(&errors) + validateSwiftFormatGuideline(&errors) + validateDocumentationGuideline(&errors) + validateLocalizationGuideline(&errors) + validateAppStoreGuideline(&errors) + validateLocalizationScripts(&errors) + validateXcodeProjectSettingsGuideline(&errors) + validatePackageCompilerSettingsGuideline(&errors) + validateExternalDependencyPolicy(&errors) + validateGitignoreGuidance(&errors) + validateExecutable(consumerSetupScript, description: "consumer setup validator", errors: &errors) + validateAuditSkill(&errors) + if !errors.isEmpty { + print("Guideline validation failed:") + for error in errors { print("- \(error)") } + return 1 + } + let guideRoot = root.appendingPathComponent("Guidelines") + let guideCount = recursiveFiles(below: guideRoot).filter { $0.pathExtension.lowercased() == "md" }.count + let version = + (try? String(contentsOf: versionFile, encoding: .utf8))? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "unknown" + print("Validated \(guideCount) guidelines for version \(version).") + return 0 +} + +exit(main()) diff --git a/AgentGuidelines/Scripts/validate_string_catalogs.swift b/AgentGuidelines/Scripts/validate_string_catalogs.swift new file mode 100755 index 0000000..c92dd38 --- /dev/null +++ b/AgentGuidelines/Scripts/validate_string_catalogs.swift @@ -0,0 +1,421 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +/// One normalized printf placeholder. +struct FormatToken: Hashable { + let position: Int + let name: String? + let format: String +} + +/// Parsed validation command-line values. +struct Arguments { + var catalogDirectories: [String] = [] + var sourceDirectories: [String] = [] + var symbolCatalogs: [String] = [] + var requiredLanguages: Set = [] +} + +let formatSpecifierPattern = + #"%(?!%)(?:([1-9]\d*)\$)?(?:\(([A-Za-z_][A-Za-z0-9_]*)\))?([-+ #0']*(?:\d+|\*)?(?:\.(?:\d+|\*))?(?:hh|h|ll|l|q|L|z|t|j)?[diouxXfFeEgGaAcCsSp@])"# + +/// Returns one capture from a regular-expression match. +func capture(_ index: Int, from match: NSTextCheckingResult, in value: String) -> String? { + let range = match.range(at: index) + guard range.location != NSNotFound, let swiftRange = Range(range, in: value) else { + return nil + } + return String(value[swiftRange]) +} + +/// Returns position, semantic name, and type for every printf placeholder. +func formatSignature(_ value: String) -> [FormatToken] { + guard let expression = try? NSRegularExpression(pattern: formatSpecifierPattern) else { + return [] + } + let range = NSRange(value.startIndex.. FormatToken in + defer { implicitPosition += 1 } + let position = capture(1, from: match, in: value).flatMap(Int.init) ?? implicitPosition + return FormatToken( + position: position, + name: capture(2, from: match, in: value), + format: capture(3, from: match, in: value) ?? "" + ) + } + return tokens.sorted { + ($0.position, $0.name ?? "", $0.format) < ($1.position, $1.name ?? "", $1.format) + } +} + +/// Returns every leaf String Catalog value below one localization. +func stringUnitValues(_ value: Any) -> [String] { + if let dictionary = value as? [String: Any] { + var values: [String] = [] + if let stringUnit = dictionary["stringUnit"] as? [String: Any], + let stringValue = stringUnit["value"] as? String + { + values.append(stringValue) + } + for (key, child) in dictionary where key != "stringUnit" { + values.append(contentsOf: stringUnitValues(child)) + } + return values + } + if let array = value as? [Any] { + return array.flatMap(stringUnitValues) + } + return [] +} + +/// Returns every leaf String Catalog state below one localization. +func stringUnitStates(_ value: Any) -> [String] { + if let dictionary = value as? [String: Any] { + var states: [String] = [] + if let stringUnit = dictionary["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String + { + states.append(state) + } + for (key, child) in dictionary where key != "stringUnit" { + states.append(contentsOf: stringUnitStates(child)) + } + return states + } + if let array = value as? [Any] { + return array.flatMap(stringUnitStates) + } + return [] +} + +/// Renders a normalized placeholder signature for diagnostics. +func displaySignature(_ signature: [FormatToken]) -> String { + let specifiers = signature.map { "%" + ($0.name.map { "(\($0))" } ?? "") + $0.format } + return specifiers.isEmpty ? "none" : specifiers.joined(separator: ", ") +} + +/// Returns translated values whose placeholder signature differs from source. +func formatSignatureIssues(_ catalog: [String: Any]) -> [String] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, + let strings = catalog["strings"] as? [String: Any] + else { + return ["catalog structure is invalid"] + } + var issues: [String] = [] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any], + entry["extractionState"] as? String != "stale", + let localizations = entry["localizations"] as? [String: Any] + else { + continue + } + let sourceSignatures = Set(stringUnitValues(localizations[sourceLanguage] as Any).map(formatSignature)) + guard sourceSignatures.count == 1, let expected = sourceSignatures.first else { + issues.append("\(key): source variants have inconsistent format specifiers") + continue + } + for language in localizations.keys.sorted() where language != sourceLanguage { + for value in stringUnitValues(localizations[language] as Any) { + let actual = formatSignature(value) + if actual != expected { + issues.append( + "\(key) [\(language)]: format specifiers \(displaySignature(actual)) " + + "do not match \(displaySignature(expected))" + ) + } + } + } + } + return issues +} + +/// Returns missing required localizations and unfinished translated values. +func translationStateIssues(_ catalog: [String: Any], requiredLanguages: Set) -> [String] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, + let strings = catalog["strings"] as? [String: Any] + else { + return ["catalog structure is invalid"] + } + var issues: [String] = [] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any], entry["extractionState"] as? String != "stale" else { + continue + } + let localizations = entry["localizations"] as? [String: Any] ?? [:] + for language in requiredLanguages.subtracting([sourceLanguage]).sorted() where localizations[language] == nil { + issues.append("\(key) [\(language)]: required localization is missing") + } + for language in localizations.keys.sorted() where language != sourceLanguage { + let states = stringUnitStates(localizations[language] as Any) + if states.isEmpty { + issues.append("\(key) [\(language)]: localization has no string units") + continue + } + let unfinished = Set(states.filter { $0 == "new" || $0 == "needs_review" }).sorted() + if !unfinished.isEmpty { + issues.append("\(key) [\(language)]: unfinished states \(unfinished.joined(separator: ", "))") + } + } + } + return issues +} + +/// Returns active catalog entries that cannot generate expected Swift symbols. +func symbolIssues(_ catalog: [String: Any]) -> [String] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, + let strings = catalog["strings"] as? [String: Any] + else { + return ["catalog structure is invalid"] + } + var issues: [String] = [] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any] else { + issues.append("\(key): entry is not a dictionary") + continue + } + if entry["extractionState"] as? String == "stale" { + continue + } + if entry["extractionState"] as? String != "manual" { + issues.append("\(key): extractionState is not manual") + } + let localizations = entry["localizations"] as? [String: Any] + if localizations?[sourceLanguage] as? [String: Any] == nil { + issues.append("\(key): source localization \(sourceLanguage) is missing") + } + } + return issues +} + +/// Recursively discovers files with an extension below a directory. +func files(withExtension pathExtension: String, below directories: [String]) -> [String] { + let fileManager = FileManager.default + var paths = Set() + for directory in directories { + guard let enumerator = fileManager.enumerator(atPath: directory) else { + continue + } + for case let candidate as String in enumerator where candidate.hasSuffix(".\(pathExtension)") { + let path = URL(fileURLWithPath: directory).appendingPathComponent(candidate).standardizedFileURL.path + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: path, isDirectory: &isDirectory), !isDirectory.boolValue { + paths.insert(path) + } + } + } + return paths.sorted() +} + +/// Runs a process and returns its standard output. +func run(_ command: [String]) throws -> Data { + let process = Process() + let output = Pipe() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = command + process.standardOutput = output + process.standardError = output + try process.run() + let outputData = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + guard process.terminationStatus == 0 else { + let detail = + String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + ?? "xcstringstool extract failed" + throw NSError( + domain: "CatalogValidation", code: Int(process.terminationStatus), + userInfo: [ + NSLocalizedDescriptionKey: detail + ]) + } + return outputData +} + +/// Returns checked-in Swift locations that still use localization literals. +func literalLocalizationReferences(sourceDirectories: [String]) throws -> [String] { + let sourcePaths = files(withExtension: "swift", below: sourceDirectories) + if sourcePaths.isEmpty { + return [] + } + let temporary = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: temporary, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: temporary) } + _ = try run( + [ + "xcrun", "xcstringstool", "extract", "--modern-localizable-strings", "--SwiftUI", + "--omit-empty-stringsdata", "--output-directory", temporary.path, + ] + sourcePaths + ) + let stringsDataPaths = files(withExtension: "stringsdata", below: [temporary.path]) + var references: [String] = [] + for path in stringsDataPaths { + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + guard let stringsData = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + continue + } + let source = stringsData["source"] as? String ?? URL(fileURLWithPath: path).lastPathComponent + let tables = stringsData["tables"] as? [String: Any] + let entries = tables?["Localizable"] as? [[String: Any]] ?? [] + for entry in entries { + let location = entry["location"] as? [String: Any] + let line: String + if let number = location?["startingLine"] as? NSNumber { + line = number.stringValue + } else { + line = "?" + } + let key = entry["key"] as? String ?? "" + references.append("\(source):\(line): \(key)") + } + } + return references.sorted() +} + +/// Parses validation command-line arguments. +func parseArguments(_ values: [String]) throws -> Arguments { + var arguments = Arguments() + var index = 0 + while index < values.count { + let value = values[index] + if value == "--help" { + print( + "Usage: validate_string_catalogs.swift --catalog-directory " + + "--source-directory [--symbol-catalog ] " + + "[--required-language ]" + ) + exit(0) + } + guard index + 1 < values.count else { + throw NSError( + domain: "CatalogValidation", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "missing value for \(value)" + ]) + } + let next = values[index + 1] + switch value { + case "--catalog-directory": arguments.catalogDirectories.append(next) + case "--source-directory": arguments.sourceDirectories.append(next) + case "--symbol-catalog": arguments.symbolCatalogs.append(URL(fileURLWithPath: next).standardizedFileURL.path) + case "--required-language": arguments.requiredLanguages.insert(next) + default: + throw NSError( + domain: "CatalogValidation", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "unknown argument: \(value)" + ]) + } + index += 2 + } + guard !arguments.catalogDirectories.isEmpty, !arguments.sourceDirectories.isEmpty else { + throw NSError( + domain: "CatalogValidation", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "--catalog-directory and --source-directory are required" + ]) + } + return arguments +} + +/// Writes text to standard error. +func writeError(_ value: String) { + FileHandle.standardError.write(Data((value + "\n").utf8)) +} + +/// Validates configured catalogs and Swift source directories. +func main() -> Int32 { + do { + let arguments = try parseArguments(Array(CommandLine.arguments.dropFirst())) + let catalogPaths = files(withExtension: "xcstrings", below: arguments.catalogDirectories) + guard !catalogPaths.isEmpty else { + writeError("No String Catalogs found in the configured directories.") + return 1 + } + let catalogSet = Set(catalogPaths) + let symbolCatalogs = + arguments.symbolCatalogs.isEmpty + ? Set(catalogPaths.filter { URL(fileURLWithPath: $0).lastPathComponent == "Localizable.xcstrings" }) + : Set(arguments.symbolCatalogs) + let unknownSymbolCatalogs = symbolCatalogs.subtracting(catalogSet) + if !unknownSymbolCatalogs.isEmpty { + for path in unknownSymbolCatalogs.sorted() { + writeError("\(path): generated-symbol catalog is outside the configured catalogs.") + } + return 1 + } + var failed = false + for catalogPath in catalogPaths { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: catalogPath)) + guard let catalog = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let strings = catalog["strings"] as? [String: Any] + else { + writeError("\(catalogPath): catalog has no strings dictionary.") + failed = true + continue + } + let staleKeys = strings.keys.filter { + (strings[$0] as? [String: Any])?["extractionState"] as? String == "stale" + }.sorted() + if !staleKeys.isEmpty { + failed = true + writeError("\(catalogPath): stale extracted strings:") + for key in staleKeys { writeError(" - \(key)") } + } + if symbolCatalogs.contains(catalogPath) { + let issues = symbolIssues(catalog) + if !issues.isEmpty { + failed = true + writeError("\(catalogPath): entries that are not symbol-ready:") + for issue in issues { writeError(" - \(issue)") } + } + } + let formatIssues = formatSignatureIssues(catalog) + if !formatIssues.isEmpty { + failed = true + writeError("\(catalogPath): format-specifier mismatches:") + for issue in formatIssues { writeError(" - \(issue)") } + } + let stateIssues = translationStateIssues(catalog, requiredLanguages: arguments.requiredLanguages) + if !stateIssues.isEmpty { + failed = true + writeError("\(catalogPath): localization-state issues:") + for issue in stateIssues { writeError(" - \(issue)") } + } + } catch { + writeError("\(catalogPath): \(error.localizedDescription)") + failed = true + } + } + do { + let references = try literalLocalizationReferences(sourceDirectories: arguments.sourceDirectories) + if !references.isEmpty { + failed = true + writeError("Checked-in Swift localization literals must use generated symbols:") + for reference in references { writeError(" - \(reference)") } + } + } catch { + writeError("Could not validate Swift localization literals: \(error.localizedDescription)") + return 1 + } + if failed { + writeError( + "Prepare generated-symbol catalogs, migrate reported Swift literals, and resolve catalog issues." + ) + return 1 + } + print("String Catalog validation passed: symbols, states, format signatures, and Swift source are valid.") + return 0 + } catch { + writeError("String Catalog validation failed: \(error.localizedDescription)") + return 2 + } +} + +exit(main()) diff --git a/AgentGuidelines/Templates/.gitignore b/AgentGuidelines/Templates/.gitignore new file mode 100644 index 0000000..3a3fc11 --- /dev/null +++ b/AgentGuidelines/Templates/.gitignore @@ -0,0 +1,66 @@ +# macOS +.DS_Store + +# Xcode user data, build output, and archives +xcuserdata/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.hmap +/build/ +/DerivedData/ +*.ipa +*.dSYM +*.dSYM.zip + +# Xcode playground generated state +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +/.build/ +/Packages/ +.swiftpm/configuration/registries.json + +# Node.js +node_modules/ +dist/ +coverage/ +*.tgz +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Local credentials and environment configuration +.env +.env.* +!.env.example +!.env.*.example +.netrc + +# ThatFactory tooling state +.appstore-connect-mcp/ +.devspace/ +/public-check/ + +# Project-specific entries require a durable rationale after repository-owner review. +# Project-specific: diff --git a/AgentGuidelines/Templates/AGENTS.md b/AgentGuidelines/Templates/AGENTS.md new file mode 100644 index 0000000..092c1a9 --- /dev/null +++ b/AgentGuidelines/Templates/AGENTS.md @@ -0,0 +1,121 @@ +# Project Instructions + +## Context + +Describe the product or package, supported platforms, and durable constraints. Link to the project README or product documentation instead of duplicating it. + +## Shared guidelines + +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) +- [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) +- [Localization](AgentGuidelines/Guidelines/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) +- [Xcode MCP and visual verification](AgentGuidelines/Guidelines/Xcode/MCP.md) +- [Xcode project settings](AgentGuidelines/Guidelines/Xcode/ProjectSettings.md) +- [Xcode security audits](AgentGuidelines/Guidelines/Xcode/Security.md) + +For an application that uses Redux, also read [Redux architecture](AgentGuidelines/Guidelines/Architecture/Redux.md). + +Keep the following observability contract in the consumer repository's root `AGENTS.md` so implementation agents treat runtime diagnostics as part of lifecycle work. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## Runtime Observability + +Treat privacy-safe runtime observability as part of implementing or changing stateful, asynchronous, fallible, or lifecycle-oriented behavior. Identify the meaningful success, failure, cancellation, recovery, and state-transition boundaries before handoff, and emit concise AppLogger events owned by the artifact that implements them. Dependency declaration or target linkage alone does not satisfy this requirement. + +Every ThatFactory package log starts with its canonical emoji and uses its own stable subsystem. Never log credentials, account or record identifiers, share URLs, captured content, images, or other user-generated values as public metadata. Keep pure values and utilities silent when they have no meaningful diagnostic event; record that deliberate decision in the implementation handoff instead of adding initializer or property-access noise. Follow [Logging](AgentGuidelines/Guidelines/Logging.md) for ownership, privacy, severity, message design, and tests. + +``` + +Keep the following marked external-dependency contract in the consumer repository's root `AGENTS.md` so implementation agents receive the rule directly before they make dependency choices. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## External Dependency Policy + +Do not introduce third-party source or binary dependencies into ThatFactory applications, games, or reusable packages during normal development. Prefer Apple platform APIs, the Swift standard library, code owned by the current repository, or focused ThatFactory-owned packages. If reusable capability is missing, implement it natively at the appropriate boundary and consider extracting it into a first-party package instead of selecting an external library. + +A third-party dependency may be added, or expanded to a new target or runtime role, only with explicit repository-owner approval for that specific use before modifying the dependency graph. Convenience, reduced implementation effort, popularity, or an agent's preference for an existing library are not sufficient justification. Do not make an external library acceptable merely by hiding it behind a first-party wrapper. + +Document every approved exception in durable repository documentation in the same change. Record the dependency and source, purpose and target scope, why a native or first-party implementation is not appropriate, relevant license, security, and maintenance considerations, and the approval context. Merely mentioning or using the dependency in an execution plan, pull-request description, or transient chat is not approval. Regardless of where explicit approval occurs, reflect the exception in durable repository documentation. + +Apple system frameworks and the Swift standard library are not third-party dependencies. ThatFactory-owned packages are first-party dependencies. Tooling explicitly required by the shared guidelines is allowed only for its documented tooling role and must not be linked into or shipped with product runtime targets unless separately approved and documented. + +Follow [Development workflow](AgentGuidelines/Guidelines/Development.md) for the detailed policy. + +``` + +Keep the following documentation-maintenance contract in the consumer repository's root `AGENTS.md` so implementation agents receive it directly rather than only through a linked guide. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## Documentation Maintenance + +Treat documentation as part of implementation, not optional follow-up. At the start of implementation, identify code-level or project-level documentation likely to describe the affected behavior; before handoff, reconcile that documentation with the final implementation. + +Update documentation when a change alters durable or core feature behavior or another documented contract. Regardless of change size, if the implementation makes existing documentation inaccurate, incomplete, misleading, or obsolete, update or remove that documentation in the same change. + +Do not create documentation churn for incidental implementation details that are not durable and do not affect an existing documented claim. Follow [Documentation](AgentGuidelines/Guidelines/Documentation.md) for detailed scope and the completion checklist. + +``` + +Keep the following marked code-review contract in the consumer repository's root `AGENTS.md` so it is loaded directly for root-level Codex and pull-request work. Copy it unchanged and update it when the marker version changes in this template; a Markdown link to the detailed workflow is not an instruction include. + +```md + +## Code Review Rules + +Review for release-blocking defects introduced or materially exposed by the pull request. A clean review means no unresolved P0/P1 findings; it does not mean exhaustive or perfect software. + +A blocking finding must identify a concrete, reachable path in a supported use case or the documented threat model that can cause a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. + +For every blocking finding, state the severity, preconditions, execution path, impact, evidence, and actionable remediation. Group manifestations that share the same root cause into one finding. + +Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, behavior by components outside the threat model, style preferences, and speculative refactoring. Record a useful lower-severity observation once as deferred, declined, duplicate, or follow-up work; do not keep the review loop open for it. + +In an initial review, report substantiated blockers together. A follow-up review is limited to unresolved P0/P1 findings, changes since the last reviewed commit, and code directly affected by those changes. Do not restart an unrestricted review of unchanged code. A new follow-up finding must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +The review-round budget below applies only to Codex GitHub reviews: the configured automatic Codex review and any manual `@codex review` request. It does not apply to ChatGPT review or reasoning delegated through Reasoning Relay. An otherwise-authorized Reasoning Relay workflow may request as many Relay review or follow-up delegations as its own governing workflow requires; those requests neither consume the Codex budget nor require repository-owner authorization under it. + +Automatic Codex review is the initial Codex review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another Codex review after each remediation commit. Within the normal Codex review budget, at most one owner-authorized, delta-scoped Codex verification review may be requested under [the pull-request review workflow](AgentGuidelines/Guidelines/GitHub/PullRequests.md). + + +## Codex review scope + +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. +``` + +The marked block is intentional controlled duplication of the shared review policy. The tracked, synchronized subtree is reviewed centrally in `thatfactory/agent-guidelines`; the root-level instructions ensure the review contract and subtree scope are loaded even when Codex starts from the repository root. + +## Physical folder map + +Replace these examples with exact repository paths: + +| Role | Physical folder | +|---|---| +| Application sources | `/` | +| Redux | `/Redux/` | +| Views | `/View/` | +| Services | `/Services/` | +| Unit tests | `Tests/` | + +## Stack + +Record the supported Xcode, Swift, and platform versions. Follow the shared Xcode project-settings baseline and record any exact, scoped exception in the local specialization or linked durable documentation. + +## 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/AgentGuidelines/Templates/GlobalCodexInstructions.md b/AgentGuidelines/Templates/GlobalCodexInstructions.md new file mode 100644 index 0000000..abe4578 --- /dev/null +++ b/AgentGuidelines/Templates/GlobalCodexInstructions.md @@ -0,0 +1,30 @@ +# 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-specific guidance in global instructions. Global instructions should bootstrap discovery of the repository's own sources of truth. + +## Code review behavior + +When acting as a code reviewer, optimize for high-signal release risk and convergence rather than exhaustive perfection. + +Create an inline finding only when all of the following are true: + +1. The issue is introduced or materially exposed by the proposed change. +2. There is a concrete, reachable failure path in a supported use case or the documented threat model. +3. The impact is P0 or P1: a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. +4. The evidence and remediation are specific enough to be actionable. + +State the finding's severity, preconditions, execution path, impact, and evidence. Group findings that share the same root cause. Do not create separate serial comments for additional manifestations of an already reported root cause. + +Treat P2 and P3 observations as non-blocking. This includes defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, adversarial behavior by components outside the threat model, style preferences, speculative refactoring, and exhaustive enumeration of equivalent input formats. Summarize valuable lower-severity observations once or recommend a follow-up issue. + +In the initial review, report substantiated blockers together rather than drip-feeding them across repeated reviews. + +In a follow-up review, verify previously reported P0/P1 findings and review only changes since the previously reviewed commit plus code directly affected by those changes. Do not restart an unrestricted search of unchanged code. A newly introduced follow-up finding must be a P0/P1 issue introduced by the remediation or genuinely hidden by the previous defect. + +A clean review means that there are no unresolved P0/P1 blockers. It does not mean perfect software, zero possible improvements, or zero technical debt. diff --git a/AgentGuidelines/Templates/Store.swift b/AgentGuidelines/Templates/Store.swift new file mode 100644 index 0000000..0120cf1 --- /dev/null +++ b/AgentGuidelines/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/AgentGuidelines/Tests/run_tests.swift b/AgentGuidelines/Tests/run_tests.swift new file mode 100755 index 0000000..847eeab --- /dev/null +++ b/AgentGuidelines/Tests/run_tests.swift @@ -0,0 +1,911 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +struct CommandResult { + let status: Int32 + let output: String + + var succeeded: Bool { status == 0 } +} + +struct TestFailure: Error, CustomStringConvertible { + let description: String +} + +let repositoryRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() +let fileManager = FileManager.default + +func require(_ condition: @autoclosure () -> Bool, _ message: String) throws { + guard condition() else { throw TestFailure(description: message) } +} + +func run(_ arguments: [String], directory: URL = repositoryRoot) throws -> CommandResult { + let process = Process() + let output = Pipe() + process.currentDirectoryURL = directory + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = arguments + process.standardOutput = output + process.standardError = output + try process.run() + let data = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + return CommandResult(status: process.terminationStatus, output: String(decoding: data, as: UTF8.self)) +} + +func withTemporaryDirectory(_ body: (URL) throws -> Void) throws { + let directory = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: directory) } + try body(directory) +} + +func write(_ value: String, to url: URL) throws { + try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) + try value.write(to: url, atomically: true, encoding: .utf8) +} + +func script(_ path: String) -> String { + repositoryRoot.appendingPathComponent(path).path +} + +func copyRepositoryFixture(to destination: URL) throws { + try fileManager.createDirectory(at: destination, withIntermediateDirectories: true) + let children = try fileManager.contentsOfDirectory(at: repositoryRoot, includingPropertiesForKeys: nil) + for child in children where ![".git", ".build"].contains(child.lastPathComponent) { + try fileManager.copyItem(at: child, to: destination.appendingPathComponent(child.lastPathComponent)) + } +} + +func git(_ arguments: String..., in directory: URL) throws { + let result = try run(["git"] + arguments, directory: directory) + try require(result.succeeded, "git \(arguments.joined(separator: " ")) failed: \(result.output)") +} + +func initializeRepository(at root: URL) throws { + try git("init", in: root) + try git("config", "user.email", "agent@example.com", in: root) + try git("config", "user.name", "Agent", in: root) +} + +func createConsumerFixture(at root: URL, isPackage: Bool, readme: String? = nil) throws { + try fileManager.createSymbolicLink( + at: root.appendingPathComponent("AgentGuidelines"), withDestinationURL: repositoryRoot) + try fileManager.copyItem( + at: repositoryRoot.appendingPathComponent("Templates/AGENTS.md"), + to: root.appendingPathComponent("AGENTS.md")) + try write("AgentGuidelines/** linguist-generated\n", to: root.appendingPathComponent(".gitattributes")) + let skillParent = root.appendingPathComponent(".agents/skills") + try fileManager.createDirectory(at: skillParent, withIntermediateDirectories: true) + try fileManager.createSymbolicLink( + at: skillParent.appendingPathComponent("agent-guidelines-audit"), + withDestinationURL: repositoryRoot.appendingPathComponent(".agents/skills/agent-guidelines-audit") + ) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".swift-format"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.swift-format")) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".editorconfig"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.editorconfig")) + try write( + """ + name: CI + on: + pull_request: + push: + branches: [main] + jobs: + swift-format: + steps: + - run: AgentGuidelines/Scripts/swift_format.sh lint-strict Package.swift Sources Tests + """ + "\n", + to: root.appendingPathComponent(".github/workflows/ci.yml") + ) + if isPackage { + try write("// swift-tools-version: 6.0\n", to: root.appendingPathComponent("Package.swift")) + } + if let readme { + try write(readme, to: root.appendingPathComponent("README.md")) + } +} + +func localization(_ value: String, state: String = "translated") -> String { + #"{"stringUnit":{"state":"\#(state)","value":"\#(value)"}}"# +} + +let tests: [(String, () throws -> Void)] = [ + ( + "repository validator accepts the source tree", + { + let result = try run([script("Scripts/validate_guidelines.swift")]) + try require(result.succeeded, result.output) + try require(result.output.contains("Validated"), "validator did not report success") + } + ), + ( + "repository validator rejects configuration drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let configuration = fixture.appendingPathComponent("Configurations/Swift/.swift-format") + var contents = try String(contentsOf: configuration, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "\"NeverForceUnwrap\" : false", with: "\"NeverForceUnwrap\" : true") + try require(contents.contains("\"NeverForceUnwrap\" : true"), "could not mutate fixture configuration") + try write(contents, to: configuration) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "configuration drift unexpectedly passed") + try require(result.output.contains("NeverForceUnwrap must be false"), result.output) + } + } + ), + ( + "repository validator rejects missing package compiler policy", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".treatAllWarnings(as: .error)", with: ".warningsAsErrors()") + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing package warning policy unexpectedly passed") + try require(result.output.contains("missing package compiler policy"), result.output) + } + } + ), + ( + "repository validator rejects package upcoming-feature drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".enableUpcomingFeature(\"NonisolatedNonsendingByDefault\")", + with: ".enableUpcomingFeature(\"ExampleFeature\")" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "package upcoming-feature drift unexpectedly passed") + try require( + result.output.contains( + "missing required SwiftPM upcoming feature 'NonisolatedNonsendingByDefault'"), + result.output + ) + } + } + ), + ( + "repository validator rejects missing package plug-in exclusion", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "package plug-in targets for which `Target.plugin(...)` does not expose `swiftSettings`", + with: "other package targets" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing package plug-in exclusion unexpectedly passed") + try require( + result.output.contains("missing package compiler policy unsupported plug-in target exclusion"), + result.output) + } + } + ), + ( + "repository validator rejects missing experimental StrictConcurrency rule", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".enableExperimentalFeature(\"StrictConcurrency\")", + with: ".enableExperimentalFeature(\"ExampleFeature\")" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing experimental StrictConcurrency rule unexpectedly passed") + try require( + result.output.contains( + "missing package compiler policy experimental StrictConcurrency redundancy rule"), result.output + ) + } + } + ), + ( + "repository validator rejects missing upcoming StrictConcurrency rule", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".enableUpcomingFeature(\"StrictConcurrency\")", + with: ".enableUpcomingFeature(\"ExampleFeature\")" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing upcoming StrictConcurrency rule unexpectedly passed") + try require( + result.output.contains( + "missing package compiler policy upcoming StrictConcurrency redundancy rule"), result.output + ) + } + } + ), + ( + "repository validator rejects missing explicit StrictConcurrency rule", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "StrictConcurrency=complete", + with: "StrictConcurrency=minimal" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing explicit StrictConcurrency rule unexpectedly passed") + try require( + result.output.contains( + "missing package compiler policy explicit StrictConcurrency redundancy rule"), result.output + ) + } + } + ), + ( + "repository validator rejects missing Xcode package-parity maintenance", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Xcode/ProjectSettings.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "../Packages.md#compiler-settings-baseline", with: "package-policy") + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing Xcode package-parity maintenance unexpectedly passed") + try require(result.output.contains("missing Swift package baseline cross-reference"), result.output) + } + } + ), + ( + "repository validator rejects missing package audit section", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let skill = fixture.appendingPathComponent(".agents/skills/agent-guidelines-audit/SKILL.md") + var contents = try String(contentsOf: skill, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "## Audit Swift package settings", with: "## Inspect Swift manifests") + try write(contents, to: skill) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing package audit section unexpectedly passed") + try require(result.output.contains("missing Swift package-settings audit"), result.output) + } + } + ), + ( + "repository validator rejects package audit behavior drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let skill = fixture.appendingPathComponent(".agents/skills/agent-guidelines-audit/SKILL.md") + var contents = try String(contentsOf: skill, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "swift package --package-path dump-package", + with: "inspect the manifest" + ) + try write(contents, to: skill) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "package audit behavior drift unexpectedly passed") + try require(result.output.contains("missing evaluated manifest inspection"), result.output) + } + } + ), + ( + "repository validator rejects missing observability adoption guidance", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let readme = fixture.appendingPathComponent("README.md") + var contents = try String(contentsOf: readme, encoding: .utf8) + contents = contents.replacingOccurrences(of: "runtime-observability contract", with: "runtime contract") + try write(contents, to: readme) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing observability adoption guidance unexpectedly passed") + try require( + result.output.contains("missing runtime observability contract synchronization"), result.output) + } + } + ), + ( + "repository validator rejects private dependency authentication drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/CICD.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "isolated disposable or ephemeral self-hosted runner", + with: "self-hosted runner") + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "private dependency authentication drift unexpectedly passed") + try require( + result.output.contains("missing untrusted-code runner isolation"), + result.output + ) + } + } + ), + ( + "repository validator rejects gitignore template drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let template = fixture.appendingPathComponent("Templates/.gitignore") + var contents = try String(contentsOf: template, encoding: .utf8) + contents = contents.replacingOccurrences(of: "node_modules/\n", with: "") + try write(contents, to: template) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "gitignore template drift unexpectedly passed") + try require( + result.output.contains("Templates/.gitignore: missing required pattern 'node_modules/'"), + result.output + ) + } + } + ), + ( + "repository validator rejects forbidden tooling in gitignore template", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let template = fixture.appendingPathComponent("Templates/.gitignore") + var contents = try String(contentsOf: template, encoding: .utf8) + contents += "fastlane/test_output/\n" + try write(contents, to: template) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "forbidden tooling pattern unexpectedly passed") + try require( + result.output.contains("must not include forbidden tooling pattern 'fastlane/'"), + result.output + ) + } + } + ), + ( + "Markdown checker reports governed wrapping", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Example.md") + try write( + """ + # Example + + This paragraph was split + across physical lines. + + - This list item was split + across physical lines too. + + > This quotation was split + > across physical lines as well. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(!result.succeeded, "wrapped prose unexpectedly passed") + for expected in [ + ":3: paragraph spans physical lines 3-4", + ":6: list item spans physical lines 6-7", + ":9: block quote (depth 1) spans physical lines 9-10", + ] { + try require(result.output.contains(expected), "missing diagnostic: \(expected)\n\(result.output)") + } + } + } + ), + ( + "Markdown checker accepts GitHub alerts", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Alerts.md") + try write( + """ + # Alerts + + > [!NOTE] + > Note body. + + > [!TIP] + > Tip body. + + > [!IMPORTANT] + > Important body. + + > [!WARNING] + > Warning body. + + > [!CAUTION] + > Caution body. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "Markdown checker rejects wrapped or malformed GitHub alerts", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Alerts.md") + try write( + """ + # Alerts + + > [!NOTE] + > This alert body was split + > across physical lines. + + > [!UNKNOWN] + > Unknown alert body. + + > > [!TIP] + > > Nested alert body. + + > Ordinary quote begins. + > [!NOTE] + > Ordinary quote continues. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(!result.succeeded, "invalid alerts unexpectedly passed") + for expected in [ + ":4: block quote (depth 1) spans physical lines 4-5", + ":7: block quote (depth 1) spans physical lines 7-8", + ":10: block quote (depth 2) spans physical lines 10-11", + ":13: block quote (depth 1) spans physical lines 13-15", + ] { + try require(result.output.contains(expected), "missing diagnostic: \(expected)\n\(result.output)") + } + } + } + ), + ( + "Markdown checker accepts verbatim structures", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Example.md") + try write( + """ + --- + title: Example + --- + + # Example + + One physical line of prose. + + | Role | Folder | + |---|---| + | App | `App/` | + + ```text + diagram + ``` + +

+ Badge +

+ + - Parent item. + - Nested item. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "String Catalog inspection fails closed for every Git change kind", + { + try withTemporaryDirectory { root in + try initializeRepository(at: root) + for name in ["Modified.xcstrings", "DeletedThenRenamed.xcstrings", "Copied.xcstrings"] { + try write("{}\n", to: root.appendingPathComponent(name)) + } + try git("add", ".", in: root) + try git("commit", "-m", "Fixture", in: root) + try write(#"{"sourceLanguage":"en"}"# + "\n", to: root.appendingPathComponent("Modified.xcstrings")) + try git("mv", "DeletedThenRenamed.xcstrings", "Renamed.xcstrings", in: root) + try fileManager.copyItem( + at: root.appendingPathComponent("Copied.xcstrings"), + to: root.appendingPathComponent("Added.xcstrings") + ) + try git("add", "Added.xcstrings", in: root) + try write("{}\n", to: root.appendingPathComponent("Untracked.xcstrings")) + let result = try run( + [ + script(".agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift"), + "--repository", root.path, "--base-ref", "HEAD", + ] + ) + try require(!result.succeeded, "missing editor evidence unexpectedly passed") + for name in ["Added.xcstrings", "Modified.xcstrings", "Renamed.xcstrings", "Untracked.xcstrings"] { + try require( + result.output.contains("missing Xcode catalog-editor inspection evidence: \(name)"), + "missing changed catalog \(name): \(result.output)") + } + try require(result.output.contains("--evidence-output is required"), result.output) + } + } + ), + ( + "String Catalog inspection records structured zero-diagnostic evidence", + { + try withTemporaryDirectory { temporary in + let root = temporary.appendingPathComponent("repository") + try fileManager.createDirectory(at: root, withIntermediateDirectories: true) + try initializeRepository(at: root) + let catalog = root.appendingPathComponent("Localizable.xcstrings") + try write("{}\n", to: catalog) + try git("add", ".", in: root) + try git("commit", "-m", "Fixture", in: root) + try write(#"{"sourceLanguage":"en"}"# + "\n", to: catalog) + let evidence = temporary.appendingPathComponent("evidence.json") + let result = try run( + [ + script(".agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift"), + "--repository", root.path, "--base-ref", "HEAD", "--inspected-catalog", + "Localizable.xcstrings", "--evidence-output", evidence.path, + ] + ) + try require(result.succeeded, result.output) + let data = try Data(contentsOf: evidence) + let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] + let catalogs = object?["catalogs"] as? [[String: Any]] + try require(object?["baseRef"] as? String == "HEAD", "base ref was not recorded") + try require( + (object?["xcodeVersion"] as? String)?.contains("Xcode") == true, "Xcode version was not recorded") + try require( + catalogs?.first?["path"] as? String == "Localizable.xcstrings", "catalog path was not recorded") + try require(catalogs?.first?["catalogEditorWarnings"] as? Int == 0, "warning count was not zero") + try require(catalogs?.first?["catalogEditorErrors"] as? Int == 0, "error count was not zero") + } + } + ), + ( + "symbol preparation preserves translations and stale entries", + { + try withTemporaryDirectory { root in + let catalog = root.appendingPathComponent("Localizable.xcstrings") + try write( + """ + {"sourceLanguage":"en","strings":{ + "Legacy value":{"comment":"Visible title","extractionState":"extracted_with_value","localizations":{"de":\(localization("Alter Wert"))}}, + "Old value":{"extractionState":"stale","localizations":{"en":\(localization("Old"))}} + },"version":"1.1"} + """ + "\n", + to: catalog + ) + let check = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path, "--check"]) + try require(!check.succeeded, "unprepared catalog unexpectedly passed") + let preparation = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path]) + try require(preparation.succeeded, preparation.output) + let data = try Data(contentsOf: catalog) + let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] + let strings = object?["strings"] as? [String: Any] + let active = strings?["Legacy value"] as? [String: Any] + let activeLocalizations = active?["localizations"] as? [String: Any] + let stale = strings?["Old value"] as? [String: Any] + try require(active?["extractionState"] as? String == "manual", "active key was not made manual") + try require(active?["comment"] as? String == "Visible title", "comment was not preserved") + try require( + activeLocalizations?["de"] != nil && activeLocalizations?["en"] != nil, + "translations were not preserved") + try require(stale?["extractionState"] as? String == "stale", "stale key was revived") + let finalCheck = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path, "--check"]) + try require(finalCheck.succeeded, finalCheck.output) + } + } + ), + ( + "symbol preparation rejects manual entries without source copy", + { + try withTemporaryDirectory { root in + let catalog = root.appendingPathComponent("Localizable.xcstrings") + try write( + #"{"sourceLanguage":"en","strings":{"semanticKey":{"extractionState":"manual","localizations":{"de":{"stringUnit":{"state":"translated","value":"Wert"}}}}}}"#, + to: catalog + ) + let result = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path]) + try require(!result.succeeded, "invalid manual key unexpectedly passed") + try require(result.output.contains("manual entry has no en source value"), result.output) + } + } + ), + ( + "String Catalog validator accepts valid symbols, states, and formats", + { + try withTemporaryDirectory { root in + let catalogs = root.appendingPathComponent("Catalogs") + let sources = root.appendingPathComponent("Sources") + try fileManager.createDirectory(at: sources, withIntermediateDirectories: true) + try write( + """ + {"sourceLanguage":"en","strings":{"summary":{"extractionState":"manual","localizations":{ + "en":\(localization("%1$(count)lld items")), + "de":\(localization("%1$(count)lld Einträge", state: "machine_translated")) + }}}} + """ + "\n", + to: catalogs.appendingPathComponent("Localizable.xcstrings") + ) + let result = try run( + [ + script("Scripts/validate_string_catalogs.swift"), "--catalog-directory", catalogs.path, + "--source-directory", sources.path, "--required-language", "de", + ] + ) + try require(result.succeeded, result.output) + } + } + ), + ( + "String Catalog validator reports independent catalog defects", + { + try withTemporaryDirectory { root in + let catalogs = root.appendingPathComponent("Catalogs") + let sources = root.appendingPathComponent("Sources") + try fileManager.createDirectory(at: sources, withIntermediateDirectories: true) + try write( + """ + {"sourceLanguage":"en","strings":{ + "count":{"extractionState":"manual","localizations":{"en":\(localization("%1$(count)lld items")),"de":\(localization("%1$(value)@ Einträge", state: "needs_review"))}}, + "Old":{"extractionState":"stale"} + }} + """ + "\n", + to: catalogs.appendingPathComponent("Localizable.xcstrings") + ) + let result = try run( + [ + script("Scripts/validate_string_catalogs.swift"), "--catalog-directory", catalogs.path, + "--source-directory", sources.path, "--required-language", "de", "--required-language", "fr", + ] + ) + try require(!result.succeeded, "invalid catalog unexpectedly passed") + for expected in [ + "stale extracted strings", "format specifiers", "required localization is missing", + "unfinished states needs_review", + ] { + try require(result.output.contains(expected), "missing diagnostic \(expected): \(result.output)") + } + } + } + ), + ( + "consumer validator rejects package README License headings", + { + for readme in [ + "# Package\n\n## License\n", "# Package\n\n#### lIcEnSe ####\n", + "# Package\n\nLicense\n-------\n", "# Package\n\nLICENSE\n=======\n", + ] { + try withTemporaryDirectory { root in + try createConsumerFixture(at: root, isPackage: true, readme: readme) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(!result.succeeded, "package License heading unexpectedly passed") + try require(result.output.contains("must not contain a dedicated License heading"), result.output) + } + } + } + ), + ( + "consumer validator rejects standalone package README license prose", + { + try withTemporaryDirectory { root in + try createConsumerFixture( + at: root, + isPackage: true, + readme: "# Example\n\nExample is available under the MIT license. See [LICENSE](LICENSE).\n" + ) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(!result.succeeded, "standalone package license prose unexpectedly passed") + try require( + result.output.contains("must not contain a standalone license-description paragraph"), + result.output + ) + } + } + ), + ( + "consumer validator accepts package license badge and absent README", + { + try withTemporaryDirectory { root in + try createConsumerFixture( + at: root, + isPackage: true, + readme: """ + # Example + + ![License](https://img.shields.io/badge/License-MIT-green.svg) + + See [LICENSE](LICENSE) when verifying redistribution terms. + + ````markdown + ```markdown + ## License section example + + Example is available under the MIT license. See [LICENSE](LICENSE). + ``` + ```` + + ~~~text + # License + ~~~ + """ + "\n" + ) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(result.succeeded, result.output) + } + try withTemporaryDirectory { root in + try createConsumerFixture(at: root, isPackage: true) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "consumer validator limits README license policy to package roots", + { + try withTemporaryDirectory { root in + try createConsumerFixture(at: root, isPackage: false, readme: "# App\n\n## License\n") + try write( + "# License\n\nStandalone documentation may discuss licensing.\n", + to: root.appendingPathComponent("Documentation/README.md") + ) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "consumer validator accepts synchronized integration", + { + try withTemporaryDirectory { root in + try fileManager.createSymbolicLink( + at: root.appendingPathComponent("AgentGuidelines"), withDestinationURL: repositoryRoot) + try fileManager.copyItem( + at: repositoryRoot.appendingPathComponent("Templates/AGENTS.md"), + to: root.appendingPathComponent("AGENTS.md")) + try write("AgentGuidelines/** linguist-generated\n", to: root.appendingPathComponent(".gitattributes")) + let skillParent = root.appendingPathComponent(".agents/skills") + try fileManager.createDirectory(at: skillParent, withIntermediateDirectories: true) + try fileManager.createSymbolicLink( + at: skillParent.appendingPathComponent("agent-guidelines-audit"), + withDestinationURL: repositoryRoot.appendingPathComponent(".agents/skills/agent-guidelines-audit") + ) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".swift-format"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.swift-format") + ) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".editorconfig"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.editorconfig") + ) + try write( + """ + name: CI + on: + pull_request: + push: + branches: [main] + jobs: + swift-format: + steps: + - run: AgentGuidelines/Scripts/swift_format.sh lint-strict Sources + """ + "\n", + to: root.appendingPathComponent(".github/workflows/ci.yml") + ) + let result = try run([script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path]) + try require(result.succeeded, result.output) + } + } + ), + ( + "consumer validator rejects copied audit skill and contract drift", + { + try withTemporaryDirectory { root in + try fileManager.createSymbolicLink( + at: root.appendingPathComponent("AgentGuidelines"), withDestinationURL: repositoryRoot) + let template = try String( + contentsOf: repositoryRoot.appendingPathComponent("Templates/AGENTS.md"), encoding: .utf8) + try write( + template.replacingOccurrences( + of: "P0/P1", with: "P0", options: [], range: template.range(of: "P0/P1")), + to: root.appendingPathComponent("AGENTS.md")) + try write("*.md text\n", to: root.appendingPathComponent(".gitattributes")) + let skill = root.appendingPathComponent(".agents/skills/agent-guidelines-audit") + try fileManager.createDirectory(at: skill, withIntermediateDirectories: true) + try write("stale copy\n", to: skill.appendingPathComponent("SKILL.md")) + let result = try run([script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path]) + try require(!result.succeeded, "invalid consumer unexpectedly passed") + for expected in ["code-review contract does not match", "linguist-generated", "must be a symlink"] { + try require(result.output.contains(expected), "missing diagnostic \(expected): \(result.output)") + } + } + } + ), +] + +var failures = 0 +for (name, test) in tests { + do { + try test() + print("PASS \(name)") + } catch { + failures += 1 + FileHandle.standardError.write(Data("FAIL \(name): \(error)\n".utf8)) + } +} + +if failures > 0 { + FileHandle.standardError.write(Data("\(failures) of \(tests.count) tests failed.\n".utf8)) + exit(1) +} + +print("All \(tests.count) native Swift tests passed.") diff --git a/AgentGuidelines/VERSION b/AgentGuidelines/VERSION new file mode 100644 index 0000000..cd9d21e --- /dev/null +++ b/AgentGuidelines/VERSION @@ -0,0 +1 @@ +0.0.33 From e08019ca2c9411b0d2b72f76acbecd68e028eafc Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Sat, 19 Sep 2026 16:20:18 +0200 Subject: [PATCH 2/2] Adopt strict Swift package settings --- .agents/skills/agent-guidelines-audit | 1 + .editorconfig | 1 + .gitattributes | 2 + .github/workflows/ci.yml | 18 +++- .gitignore | 68 ++++++++++++++- .swift-format | 1 + AGENTS.md | 119 ++++++++++++++++++++++++++ CHANGELOG.md | 12 +++ Package.swift | 19 +++- README.md | 11 +-- Sources/Toolbox/CodableError.swift | 25 +++--- Sources/Toolbox/JSON.swift | 14 +-- Tests/ToolboxTests/ToolboxTests.swift | 1 + VERSION | 1 + 14 files changed, 264 insertions(+), 29 deletions(-) create mode 120000 .agents/skills/agent-guidelines-audit create mode 120000 .editorconfig create mode 100644 .gitattributes create mode 120000 .swift-format create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 VERSION diff --git a/.agents/skills/agent-guidelines-audit b/.agents/skills/agent-guidelines-audit new file mode 120000 index 0000000..9e33ff8 --- /dev/null +++ b/.agents/skills/agent-guidelines-audit @@ -0,0 +1 @@ +../../AgentGuidelines/.agents/skills/agent-guidelines-audit \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 120000 index 0000000..1e825fd --- /dev/null +++ b/.editorconfig @@ -0,0 +1 @@ +AgentGuidelines/Configurations/Swift/.editorconfig \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..38ec4db --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Synced from thatfactory/agent-guidelines; keep tracked but collapse GitHub diffs. +AgentGuidelines/** linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd6d684..6590b5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,13 +11,29 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: + swift-format: + name: Swift Format + runs-on: [self-hosted, macOS] + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Validate Consumer Integration + run: AgentGuidelines/Scripts/validate_consumer_setup.swift + + - name: Run Strict Swift Format + run: AgentGuidelines/Scripts/swift_format.sh lint-strict Package.swift Sources Tests + test: name: Test runs-on: [self-hosted, macOS] steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: clean: true diff --git a/.gitignore b/.gitignore index 17f73d0..3a3fc11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,66 @@ +# macOS .DS_Store -/.build -/Packages -/*.xcodeproj + +# Xcode user data, build output, and archives xcuserdata/ -Package.resolved +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.hmap +/build/ +/DerivedData/ +*.ipa +*.dSYM +*.dSYM.zip + +# Xcode playground generated state +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +/.build/ +/Packages/ +.swiftpm/configuration/registries.json + +# Node.js +node_modules/ +dist/ +coverage/ +*.tgz +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Local credentials and environment configuration +.env +.env.* +!.env.example +!.env.*.example +.netrc + +# ThatFactory tooling state +.appstore-connect-mcp/ +.devspace/ +/public-check/ + +# Project-specific entries require a durable rationale after repository-owner review. +# Project-specific: diff --git a/.swift-format b/.swift-format new file mode 120000 index 0000000..06f3229 --- /dev/null +++ b/.swift-format @@ -0,0 +1 @@ +AgentGuidelines/Configurations/Swift/.swift-format \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a783f2d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,119 @@ +# Toolbox + +## Context + +Toolbox is a reusable Swift package. Read [README.md](README.md) before changing public behavior, supported platforms, or package integration. + +## Shared guidelines + +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) +- [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) +- [Localization](AgentGuidelines/Guidelines/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) +- [Xcode MCP and visual verification](AgentGuidelines/Guidelines/Xcode/MCP.md) +- [Xcode project settings](AgentGuidelines/Guidelines/Xcode/ProjectSettings.md) +- [Xcode security audits](AgentGuidelines/Guidelines/Xcode/Security.md) + +For an application that uses Redux, also read [Redux architecture](AgentGuidelines/Guidelines/Architecture/Redux.md). + +Keep the following observability contract in the consumer repository's root `AGENTS.md` so implementation agents treat runtime diagnostics as part of lifecycle work. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## Runtime Observability + +Treat privacy-safe runtime observability as part of implementing or changing stateful, asynchronous, fallible, or lifecycle-oriented behavior. Identify the meaningful success, failure, cancellation, recovery, and state-transition boundaries before handoff, and emit concise AppLogger events owned by the artifact that implements them. Dependency declaration or target linkage alone does not satisfy this requirement. + +Every ThatFactory package log starts with its canonical emoji and uses its own stable subsystem. Never log credentials, account or record identifiers, share URLs, captured content, images, or other user-generated values as public metadata. Keep pure values and utilities silent when they have no meaningful diagnostic event; record that deliberate decision in the implementation handoff instead of adding initializer or property-access noise. Follow [Logging](AgentGuidelines/Guidelines/Logging.md) for ownership, privacy, severity, message design, and tests. + +``` + +Keep the following marked external-dependency contract in the consumer repository's root `AGENTS.md` so implementation agents receive the rule directly before they make dependency choices. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## External Dependency Policy + +Do not introduce third-party source or binary dependencies into ThatFactory applications, games, or reusable packages during normal development. Prefer Apple platform APIs, the Swift standard library, code owned by the current repository, or focused ThatFactory-owned packages. If reusable capability is missing, implement it natively at the appropriate boundary and consider extracting it into a first-party package instead of selecting an external library. + +A third-party dependency may be added, or expanded to a new target or runtime role, only with explicit repository-owner approval for that specific use before modifying the dependency graph. Convenience, reduced implementation effort, popularity, or an agent's preference for an existing library are not sufficient justification. Do not make an external library acceptable merely by hiding it behind a first-party wrapper. + +Document every approved exception in durable repository documentation in the same change. Record the dependency and source, purpose and target scope, why a native or first-party implementation is not appropriate, relevant license, security, and maintenance considerations, and the approval context. Merely mentioning or using the dependency in an execution plan, pull-request description, or transient chat is not approval. Regardless of where explicit approval occurs, reflect the exception in durable repository documentation. + +Apple system frameworks and the Swift standard library are not third-party dependencies. ThatFactory-owned packages are first-party dependencies. Tooling explicitly required by the shared guidelines is allowed only for its documented tooling role and must not be linked into or shipped with product runtime targets unless separately approved and documented. + +Follow [Development workflow](AgentGuidelines/Guidelines/Development.md) for the detailed policy. + +``` + +Keep the following documentation-maintenance contract in the consumer repository's root `AGENTS.md` so implementation agents receive it directly rather than only through a linked guide. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## Documentation Maintenance + +Treat documentation as part of implementation, not optional follow-up. At the start of implementation, identify code-level or project-level documentation likely to describe the affected behavior; before handoff, reconcile that documentation with the final implementation. + +Update documentation when a change alters durable or core feature behavior or another documented contract. Regardless of change size, if the implementation makes existing documentation inaccurate, incomplete, misleading, or obsolete, update or remove that documentation in the same change. + +Do not create documentation churn for incidental implementation details that are not durable and do not affect an existing documented claim. Follow [Documentation](AgentGuidelines/Guidelines/Documentation.md) for detailed scope and the completion checklist. + +``` + +Keep the following marked code-review contract in the consumer repository's root `AGENTS.md` so it is loaded directly for root-level Codex and pull-request work. Copy it unchanged and update it when the marker version changes in this template; a Markdown link to the detailed workflow is not an instruction include. + +```md + +## Code Review Rules + +Review for release-blocking defects introduced or materially exposed by the pull request. A clean review means no unresolved P0/P1 findings; it does not mean exhaustive or perfect software. + +A blocking finding must identify a concrete, reachable path in a supported use case or the documented threat model that can cause a credible security-boundary bypass, durable data loss or corruption, a crash or deadlock, loss of availability, violation of an explicit acceptance criterion, or a serious compatibility regression. + +For every blocking finding, state the severity, preconditions, execution path, impact, evidence, and actionable remediation. Group manifestations that share the same root cause into one finding. + +Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretical completeness, unsupported use cases, malformed state that trusted code cannot produce, behavior by components outside the threat model, style preferences, and speculative refactoring. Record a useful lower-severity observation once as deferred, declined, duplicate, or follow-up work; do not keep the review loop open for it. + +In an initial review, report substantiated blockers together. A follow-up review is limited to unresolved P0/P1 findings, changes since the last reviewed commit, and code directly affected by those changes. Do not restart an unrestricted review of unchanged code. A new follow-up finding must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. + +The review-round budget below applies only to Codex GitHub reviews: the configured automatic Codex review and any manual `@codex review` request. It does not apply to ChatGPT review or reasoning delegated through Reasoning Relay. An otherwise-authorized Reasoning Relay workflow may request as many Relay review or follow-up delegations as its own governing workflow requires; those requests neither consume the Codex budget nor require repository-owner authorization under it. + +Automatic Codex review is the initial Codex review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another Codex review after each remediation commit. Within the normal Codex review budget, at most one owner-authorized, delta-scoped Codex verification review may be requested under [the pull-request review workflow](AgentGuidelines/Guidelines/GitHub/PullRequests.md). + + +## Codex review scope + +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. +``` + +The marked block is intentional controlled duplication of the shared review policy. The tracked, synchronized subtree is reviewed centrally in `thatfactory/agent-guidelines`; the root-level instructions ensure the review contract and subtree scope are loaded even when Codex starts from the repository root. + +## Physical folder map + +| Role | Physical folder | +|---|---| +| Package sources | `Sources/Toolbox/` | +| Unit tests | `Tests/ToolboxTests/` | + +## Stack + +- Swift 6.4 and Xcode 27. +- Platforms are declared in `Package.swift`. +- CI runner labels are `self-hosted` and `macOS`. +- Run `AgentGuidelines/Scripts/swift_format.sh format-and-lint Package.swift Sources Tests`, `swift test`, and `AgentGuidelines/Scripts/validate_consumer_setup.swift` before handoff. + +## Local specialization + +Keep the package focused on its existing public capability and preserve source compatibility unless a release explicitly documents a breaking change. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..44163dc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to toolbox are documented here. + +## 0.1.2 — 2026-09-19 + +### Changed + +- Adopted Agent Guidelines `0.0.33` and the Swift package compiler-settings baseline. +- Declared Swift 6, warnings as errors, and the required upcoming language features for every package target. +- Made imports and existential types explicit where required by the stricter compiler policy without intentionally changing runtime behavior. + diff --git a/Package.swift b/Package.swift index 94535f5..7c08fce 100644 --- a/Package.swift +++ b/Package.swift @@ -2,11 +2,20 @@ import PackageDescription +let strictSwiftSettings: [SwiftSetting] = [ + .treatAllWarnings(as: .error), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), +] + let package = Package( name: "Toolbox", platforms: [ .iOS(.v26), - .macOS(.v26) + .macOS(.v26), ], products: [ .library( @@ -21,6 +30,12 @@ let package = Package( .testTarget( name: "ToolboxTests", dependencies: ["Toolbox"] - ) + ), ] ) + +package.swiftLanguageModes = [.v6] + +for target in package.targets { + target.swiftSettings = strictSwiftSettings +} diff --git a/README.md b/README.md index d75867a..2ea5477 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,11 @@ A collection of useful Swift tools. ## Tools -Tool | Description ---- | --- -`CodableError` | Defines a `Codable` wrapper for Apple's `Error`. -`jsonDataFromFile(_:)` | Loads the contents of a JSON resource bundled with the app or test target. Returns a `Data` instance containing the raw bytes of the JSON file. + +| Tool | Description | +| --- | --- | +| `CodableError` | Defines a `Codable` wrapper for Apple's `Error`. | +| `jsonDataFromFile(_:)` | Loads the contents of a JSON resource bundled with the app or test target. Returns a `Data` instance containing the raw bytes of the JSON file. | ## Integration ### Xcode @@ -30,7 +31,7 @@ In your `Package.swift`, add `Toolbox` as a dependency: dependencies: [ .package( url: "https://github.com/thatfactory/toolbox", - from: "0.1.1" + from: "0.1.2" ) ] ``` diff --git a/Sources/Toolbox/CodableError.swift b/Sources/Toolbox/CodableError.swift index 56e00a2..68b4524 100644 --- a/Sources/Toolbox/CodableError.swift +++ b/Sources/Toolbox/CodableError.swift @@ -13,7 +13,7 @@ public struct CodableError: Error, Codable, Equatable { /// Initializes a `CodableError` instance with the given `Error`. /// /// - Parameter error: An `Error` instance. - public init(_ error: Error) { + public init(_ error: any Error) { self.errorType = String(reflecting: type(of: error)) self.description = (error as NSError).description self.localizedDescription = (error as NSError).localizedDescription @@ -23,16 +23,19 @@ public struct CodableError: Error, Codable, Equatable { /// Initializes a `CodableError` instance with the given parameters. /// - /// - Parameter errorType: `String` - /// - Parameter description: `String` - /// - Parameter localizedDescription: `String` - /// - Parameter domain: `String` - /// - Parameter code: `Int` - public init(errorType: String, - description: String, - localizedDescription: String, - domain: String, - code: Int) { + /// - Parameters: + /// - errorType: `String` + /// - description: `String` + /// - localizedDescription: `String` + /// - domain: `String` + /// - code: `Int` + public init( + errorType: String, + description: String, + localizedDescription: String, + domain: String, + code: Int + ) { self.errorType = errorType self.description = description self.localizedDescription = localizedDescription diff --git a/Sources/Toolbox/JSON.swift b/Sources/Toolbox/JSON.swift index af9d1c6..057d06b 100644 --- a/Sources/Toolbox/JSON.swift +++ b/Sources/Toolbox/JSON.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation /// Loads the contents of a JSON resource bundled with the app or test target. /// @@ -12,11 +12,13 @@ import Foundation public func jsonDataFromFile(_ fileName: String) throws(JSONError) -> Data { let fileExtension = "json" - guard let bundle = Bundle.allBundles.first( - where: { - $0.url(forResource: fileName, withExtension: fileExtension) != nil - } - ) else { + guard + let bundle = Bundle.allBundles.first( + where: { + $0.url(forResource: fileName, withExtension: fileExtension) != nil + } + ) + else { throw .noBundleForResource(fileName) } diff --git a/Tests/ToolboxTests/ToolboxTests.swift b/Tests/ToolboxTests/ToolboxTests.swift index 569e21d..773fa56 100644 --- a/Tests/ToolboxTests/ToolboxTests.swift +++ b/Tests/ToolboxTests/ToolboxTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import Toolbox final class ToolboxTests: XCTestCase { diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..d917d3e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.2