From f39e61d9a34cf4bc1f88377f11990910fa422b0b Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Wed, 9 Sep 2026 01:38:18 +0200 Subject: [PATCH 1/4] Restore exact AgentGuidelines 0.0.9 provenance --- AgentGuidelines/.github/workflows/ci.yml | 23 ++++++++++ AgentGuidelines/.github/workflows/release.yml | 44 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 AgentGuidelines/.github/workflows/ci.yml create mode 100644 AgentGuidelines/.github/workflows/release.yml diff --git a/AgentGuidelines/.github/workflows/ci.yml b/AgentGuidelines/.github/workflows/ci.yml new file mode 100644 index 0000000..5831117 --- /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: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Validate + run: | + python3 -m unittest discover -s Tests + python3 Scripts/validate_guidelines.py diff --git a/AgentGuidelines/.github/workflows/release.yml b/AgentGuidelines/.github/workflows/release.yml new file mode 100644 index 0000000..20b4c8f --- /dev/null +++ b/AgentGuidelines/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + tags: + - "*.*.*" + +permissions: + contents: write + +jobs: + release: + name: Create GitHub release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Validate guidelines + run: python3 Scripts/validate_guidelines.py + + - name: Validate tag + run: | + version="$(tr -d '[:space:]' < VERSION)" + test "$GITHUB_REF_NAME" = "$version" + + - name: Prepare release notes + run: | + version="$(tr -d '[:space:]' < VERSION)" + awk -v version="$version" ' + index($0, "## [" version "]") == 1 { capture = 1; next } + capture && /^## \[/ { exit } + capture { print } + ' CHANGELOG.md > release-notes.md + test -s release-notes.md + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + --verify-tag \ + --title "$GITHUB_REF_NAME" \ + --notes-file release-notes.md From 86eb7b09ebb4da298f9a65a2a88ae694b61e1f94 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Wed, 9 Sep 2026 01:38:27 +0200 Subject: [PATCH 2/4] Fix Swift 6.3.3 package resolution for release 1.1.1 --- CHANGELOG.md | 6 ++++++ Package.swift | 2 +- README.md | 4 ++-- VERSION | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a50ddc3..6d32f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project are documented in this file. +## [1.1.1] - 2026-09-09 + +### Fixed + +- Lowered the minimum Swift tools version from `6.4` to `6.3.3` so Xcode Cloud environments using Swift `6.3.3` can resolve the package. Public APIs and platform requirements are unchanged. + ## [1.1.0] - 2026-07-21 ### Added diff --git a/Package.swift b/Package.swift index 36d27f6..5501adf 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.4 +// swift-tools-version:6.3.3 import PackageDescription diff --git a/README.md b/README.md index 6e33a24..5bb3b48 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Swift Version + Swift Version Xcode Version Platforms SPM @@ -57,7 +57,7 @@ In your `Package.swift`, add `AppLogger` as a dependency: dependencies: [ .package( url: "https://github.com/thatfactory/applogger", - from: "1.0.0" + from: "1.1.1" ) ] ``` diff --git a/VERSION b/VERSION index 9084fa2..524cb55 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.1.1 From 5ffd78071460f87d990a936947a5fa6337403db4 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Wed, 9 Sep 2026 01:41:17 +0200 Subject: [PATCH 3/4] Update AgentGuidelines from 0.0.9 to 0.0.27 --- .../skills/agent-guidelines-audit/SKILL.md | 123 ++++ .../agent-guidelines-audit/agents/openai.yaml | 4 + .../scripts/check_markdown_wrapping.swift | 261 +++++++ .../scripts/check_xcstrings_inspection.swift | 260 +++++++ AgentGuidelines/.github/workflows/ci.yml | 6 +- AgentGuidelines/.github/workflows/release.yml | 6 +- AgentGuidelines/AGENTS.md | 27 +- AgentGuidelines/CHANGELOG.md | 173 +++++ .../Configurations/Swift/.editorconfig | 10 + .../Configurations/Swift/.swift-format | 81 +++ AgentGuidelines/Guidelines/AgentWorkflow.md | 57 ++ .../Guidelines/Architecture/Redux.md | 97 ++- AgentGuidelines/Guidelines/Development.md | 28 + AgentGuidelines/Guidelines/Documentation.md | 51 +- .../Guidelines/Git/Repositories.md | 10 + .../Guidelines/GitHub/PullRequests.md | 101 ++- AgentGuidelines/Guidelines/Localization.md | 92 +++ AgentGuidelines/Guidelines/Packages.md | 37 +- .../Guidelines/Swift/Localization.md | 43 -- AgentGuidelines/Guidelines/Swift/Swift.md | 7 +- .../Guidelines/Swift/SwiftFormat.md | 108 +++ AgentGuidelines/Guidelines/Swift/SwiftLint.md | 8 - .../Guidelines/Swift/SwiftStyle.md | 34 +- AgentGuidelines/Guidelines/Swift/SwiftUI.md | 4 +- .../Guidelines/Xcode/ProjectSettings.md | 72 ++ AgentGuidelines/README.md | 89 ++- .../Scripts/prepare_localizable_symbols.swift | 236 +++++++ AgentGuidelines/Scripts/swift_format.sh | 63 ++ .../Scripts/validate_consumer_setup.swift | 388 ++++++++++ .../Scripts/validate_guidelines.py | 125 ---- .../Scripts/validate_guidelines.swift | 660 ++++++++++++++++++ .../Scripts/validate_string_catalogs.swift | 421 +++++++++++ AgentGuidelines/Templates/AGENTS.md | 65 +- .../Templates/GlobalCodexInstructions.md | 30 + AgentGuidelines/Templates/Store.swift | 112 +++ AgentGuidelines/Tests/run_tests.swift | 519 ++++++++++++++ .../Tests/test_validate_guidelines.py | 52 -- AgentGuidelines/VERSION | 2 +- 38 files changed, 4133 insertions(+), 329 deletions(-) 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/Configurations/Swift/.editorconfig create mode 100644 AgentGuidelines/Configurations/Swift/.swift-format create mode 100644 AgentGuidelines/Guidelines/AgentWorkflow.md create mode 100644 AgentGuidelines/Guidelines/Localization.md delete mode 100644 AgentGuidelines/Guidelines/Swift/Localization.md create mode 100644 AgentGuidelines/Guidelines/Swift/SwiftFormat.md delete mode 100644 AgentGuidelines/Guidelines/Swift/SwiftLint.md create mode 100644 AgentGuidelines/Guidelines/Xcode/ProjectSettings.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 delete mode 100644 AgentGuidelines/Scripts/validate_guidelines.py create mode 100755 AgentGuidelines/Scripts/validate_guidelines.swift create mode 100755 AgentGuidelines/Scripts/validate_string_catalogs.swift create mode 100644 AgentGuidelines/Templates/GlobalCodexInstructions.md create mode 100644 AgentGuidelines/Templates/Store.swift create mode 100755 AgentGuidelines/Tests/run_tests.swift delete mode 100644 AgentGuidelines/Tests/test_validate_guidelines.py 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..66de9db --- /dev/null +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md @@ -0,0 +1,123 @@ +--- +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. + +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. +- Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, and noise controls 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. +- 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. +- For Swift-focused applications, games, and packages, inspect new repository-owned executable scripts and require Swift implemented with the standard library and Foundation. Accept an existing non-Swift script only when the nearest applicable `AGENTS.md` or durable repository documentation explicitly records the narrow exception; do not infer permission for new non-Swift automation from an existing exception. +- 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`; and require `SWIFT_VERSION` to select that newest stable language mode. +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 unit-test and UI-test targets, and inspect effective values with Xcode project-aware tooling or `xcodebuild -showBuildSettings`. 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. +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 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 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; +- 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 index 5831117..af5e61e 100644 --- a/AgentGuidelines/.github/workflows/ci.yml +++ b/AgentGuidelines/.github/workflows/ci.yml @@ -12,12 +12,12 @@ permissions: jobs: validate: name: Validate guidelines - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Validate run: | - python3 -m unittest discover -s Tests - python3 Scripts/validate_guidelines.py + Tests/run_tests.swift + Scripts/validate_guidelines.swift diff --git a/AgentGuidelines/.github/workflows/release.yml b/AgentGuidelines/.github/workflows/release.yml index 20b4c8f..6e27cca 100644 --- a/AgentGuidelines/.github/workflows/release.yml +++ b/AgentGuidelines/.github/workflows/release.yml @@ -11,13 +11,15 @@ permissions: jobs: release: name: Create GitHub release - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Validate guidelines - run: python3 Scripts/validate_guidelines.py + run: | + Tests/run_tests.swift + Scripts/validate_guidelines.swift - name: Validate tag run: | diff --git a/AgentGuidelines/AGENTS.md b/AgentGuidelines/AGENTS.md index 7c39de6..36e9089 100644 --- a/AgentGuidelines/AGENTS.md +++ b/AgentGuidelines/AGENTS.md @@ -19,6 +19,7 @@ This public repository is the versioned source of truth for reusable ThatFactory - 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: @@ -36,12 +37,36 @@ This public repository is the versioned source of truth for reusable ThatFactory --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 -python3 Scripts/validate_guidelines.py +Scripts/validate_guidelines.swift ``` Fix every validation failure before releasing a version. diff --git a/AgentGuidelines/CHANGELOG.md b/AgentGuidelines/CHANGELOG.md index 995b21d..0968e55 100644 --- a/AgentGuidelines/CHANGELOG.md +++ b/AgentGuidelines/CHANGELOG.md @@ -2,6 +2,179 @@ All notable changes to this project are documented in this file. +## [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 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/Architecture/Redux.md b/AgentGuidelines/Guidelines/Architecture/Redux.md index 56ab6f3..4a28847 100644 --- a/AgentGuidelines/Guidelines/Architecture/Redux.md +++ b/AgentGuidelines/Guidelines/Architecture/Redux.md @@ -11,7 +11,7 @@ Use this guide for applications that explicitly adopt the ThatFactory Redux arch - Middleware performs asynchronous work and other side effects. - Services wrap external frameworks, packages, persistence, clocks, APIs, and system capabilities. - Selectors derive shared domain information from state. -- Render-ready view state and view-only projections live beside their consuming views. +- Render-ready value models live under `Model/`; SwiftUI `View` types stay under `View/`. - Every side-effect result returns to the store as an action before it changes state. ## Data flow @@ -41,15 +41,7 @@ The store reduces the original action first, then awaits middleware and sequenti ## Store -Use one observable store as the source of truth and inject it at the application root. A store implementation may expose aliases like these: - -```swift -typealias AppStore = Store -typealias StateType = Equatable & Codable -typealias ActionType = Equatable -typealias Reducer = (State, Action) -> State -typealias Middleware = (State, Action) async -> Action? -``` +Use one observable store as the source of truth and inject it at the application root. The canonical Store requires `Default Actor Isolation` set to `MainActor` and `nonisolated(nonsending) By Default` set to `Yes` in every application and test target that compiles or exercises it. New projects copy [the Store template](../../Templates/Store.swift) as is; do not add redundant isolation annotations or change its dispatch ordering, observation exclusions, or documentation. Dispatch is asynchronous and ordered: @@ -61,6 +53,31 @@ Dispatch is asynchronous and ordered: Use only `await store.dispatch(_:)`. Do not add a fire-and-forget dispatch API. +## Dependency composition + +Create one application-owned `DependencyContainer` that constructs and retains services, persistence, providers, and other side-effect dependencies. Create the container before the store, restore synchronous initial state through its dependencies, and pass the container to `makeMiddlewares(_:)`. + +```swift +@main +struct ExampleApp: App { + @State private var dependencies: DependencyContainer + @State private var store: AppStore + + init() { + let dependencies = DependencyContainer() + let store = AppStore( + initialState: dependencies.restoredAppState(), + middlewares: makeMiddlewares(dependencies), + reducer: appReducer + ) + _dependencies = State(initialValue: dependencies) + _store = State(initialValue: store) + } +} +``` + +Keep application bootstrap responsible for composition, not feature behavior. Do not construct individual services directly in the app after a dependency container exists. + ## Canonical physical folders These are filesystem folders, not Xcode groups. New single-application repositories use this structure by default: @@ -79,7 +96,6 @@ These are filesystem folders, not Xcode groups. New single-application repositor |-- Services/ |-- Tools/ |-- View/ -| `-- / `-- Resources/ Tests/ @@ -94,7 +110,6 @@ These are filesystem folders, not Xcode groups. New single-application repositor |-- Services/ |-- Tools/ `-- View/ - `-- / ``` A multi-target application may use a shared source root such as `Shared/Redux/` and target-specific roots such as `/View/`. Its root `AGENTS.md` must provide a concrete path map: @@ -119,7 +134,18 @@ Put application bootstrap, app delegates, scene definitions, store construction, ### Model -Put reusable domain values in `Model/`. Keep each important type in a focused file. Do not hide response models, payloads, or domain values inside action or service files merely because only one caller currently uses them. +Put domain and presentation values in `Model/`. Models describe data, state, configuration, categories, or render-ready values; their primary responsibility is not executing an algorithm or coordinating side effects. Keep each important type in a focused file. Do not hide response models, payloads, logging categories, levels, or other values inside action or service folders merely because only one caller currently uses them. + +When several models are familiar parts of one domain, group them by that domain: + +```text +Model/ +|-- Camera/ +|-- Face/ +`-- Logging/ +``` + +Use names that help a reader reason about the domain. Keep `Model/` flat while a domain has only one file; do not create a folder for every type. ### Action @@ -134,6 +160,10 @@ enum AppAction: Equatable { Name actions after what happened or what the user requested. Keep cases in the order required by the project's Swift style guide. +Declare `AppAction` and each domain action in separate files. `AppAction.swift` contains the root routing action only; do not append logging models, categories, feature actions, or unrelated supporting declarations to it. + +Every production file under `Redux/Action/` must define an action. Values carried by actions, including categories, levels, payloads, and capability descriptions, belong in `Model/`. + ### State Put the root state and domain sub-states in `Redux/State/`. Prefer focused value types with compiler-synthesized conformances. Add a new sub-state for a durable domain instead of folding unrelated values into an existing feature. @@ -142,6 +172,8 @@ State stores durable facts. Avoid storing values that are cheap, deterministic d Sub-states should conform to `Equatable` and `Codable`; add `Sendable` when their values and concurrency boundaries require it. Keep root state and root actions for genuine cross-domain behavior. Keep domain action cases descriptive of intent or outcomes and route them through the root action. +Declare `AppState` and each domain sub-state in separate files. `AppState.swift` contains the root state only. + ### Reducer Put reducer functions in `Redux/Reducer/`. A reducer receives state and an action and returns new state. It must not: @@ -155,13 +187,17 @@ Put reducer functions in `Redux/Reducer/`. A reducer receives state and an actio Use the smallest state and action inputs that correctly express the transition. Root reducers compose domain reducers. +Declare the root reducer and each domain reducer in separate files. `AppReducer.swift` contains only root composition. Every production file under `Redux/Reducer/` must define a reducer; move events, capability values, policies, and other supporting domain types to `Model/` or their own appropriate component. + ### Middleware Put middleware in `Redux/Middleware/`. Middleware may call injected services and return a follow-up action. It must not mutate store state directly. Inject services, providers, managers, clocks, and identifier generators through parameters so middleware tests remain deterministic. Register middleware in one root composition file such as `AppMiddlewares.swift`. Reducers own every state mutation. -Create a feature subfolder when a domain has multiple middleware files: +Every production file under `Redux/Middleware/` must define or compose middleware. A helper, closure signature, or type alias used only by one middleware stays in that middleware file and should be private when its test seam and call sites allow it. Do not create a standalone middleware file for a declaration that is not middleware. + +Create a feature subfolder only when a domain has multiple middleware files: ```text Redux/Middleware/Account/ @@ -178,32 +214,33 @@ Do not put SwiftUI types, colors, images, localized display strings, or render-r ### Services -Put focused external-boundary abstractions in `Services//`. Services wrap APIs, persistence, packages, frameworks, sensors, system features, and other impure operations. Middleware calls services; views and reducers do not. +Put focused external-boundary abstractions in `Services/`. Services wrap APIs, persistence, packages, frameworks, sensors, system features, and other impure operations. Keep this folder flat while a capability has only one file; introduce a familiar capability folder such as `Services/FaceService/` or `Services/CalibrationService/` when that capability genuinely requires several related files. Middleware calls services; views and reducers do not. Prefer a protocol or otherwise injectable contract when a service must be replaced in tests. Keep transport-specific details behind the service boundary. +Keep a supporting delegate, adapter, or helper beside its service when only that capability uses it. Local ownership is clearer than promoting a service-private framework bridge to a global `Tools/` folder. + Views dispatch actions; middleware calls services. Views never call a service directly for Redux-owned behavior. ### Tools -Put genuinely cross-cutting implementation utilities in `Tools/`. This is not a miscellaneous folder. Feature-only formatters, helpers, constants, or factories stay beside that feature. Promote them to `Tools/` only after they have a clear cross-feature role. +Put specialized algorithms, accumulators, framework adapters, and genuinely cross-cutting implementation utilities in `Tools/`. This is not a miscellaneous folder. A type belongs here when its primary responsibility is performing computation or implementing technical behavior rather than describing values or owning an external capability. Feature-only helpers stay beside that feature. Keep `Tools/` flat until one familiar topic requires several files, then group them under a domain folder such as `Tools/Face/`. ### View -Put SwiftUI screens and components in `View//`. A new view belongs to the feature it renders, not in Redux. Reusable visual components may use `View/Generic/` or another explicitly declared shared-view folder. +Put SwiftUI screens and components in `View/`. A new view belongs to the feature it renders, not in Redux. Reusable visual components may use `View/Generic/` or another explicitly declared shared-view folder. Keep `View/` flat while it has only a few files; introduce `View//` when a familiar feature genuinely has several views. -Render-facing view-state types and projections live beside the consuming view: +Render-facing value types that do not conform to `View` are presentation models and live under `Model//`: ```text +Model/Account/ +`-- AccountViewState.swift + View/Account/ -|-- AccountView.swift -|-- AccountViewState.swift -`-- AccountViewStateProjection.swift +`-- AccountView.swift ``` -If a projection exists only to render one screen, it is view-layer code even when its input is `AppState`. - -Projection tests mirror the production view path under the test target. +Keep a tiny private projection beside its consuming view only when it is an implementation detail rather than a named value type. ### Resources @@ -213,10 +250,12 @@ Put catalogs, assets, preview assets, configuration resources, and test plans in - Prefer one primary concern per file. - When a feature has several files of one Redux component, introduce a feature subfolder under that component. +- Group several related models, services, or tools by a familiar domain or capability so readers can reason about them together. - Keep root routing and composition at the component root; keep feature implementations below it. - File names match their primary type or clearly describe their primary pure function. - Do not introduce artificial enum namespaces solely to satisfy filename lint rules. - Mirror production organization in tests so components are easy to locate. +- Do not keep empty component folders. Add `Selector/`, `Tools/`, feature folders, or mirrored test folders only when they contain a real implementation. ## SwiftUI connection @@ -230,18 +269,18 @@ Prefer narrow view inputs or a focused view-state projection. This aligns SwiftU | Step | Change | Default destination | |---|---|---| -| 1 | Define domain models | `Model//` | +| 1 | Define domain models | `Model/` or `Model//` when several are familiar | | 2 | Define feature state | `Redux/State/State.swift` | | 3 | Add it to root state | `Redux/State/AppState.swift` | | 4 | Define feature actions | `Redux/Action/Action.swift` | | 5 | Route them through the root action | `Redux/Action/AppAction.swift` | | 6 | Implement the reducer | `Redux/Reducer/Reducer.swift` | | 7 | Compose the reducer | `Redux/Reducer/AppReducer.swift` | -| 8 | Add side effects if needed | `Redux/Middleware//` | +| 8 | Add side effects if needed | `Redux/Middleware/` or a feature folder when several | | 9 | Register middleware | `Redux/Middleware/AppMiddlewares.swift` | -| 10 | Add external boundaries if needed | `Services//` | +| 10 | Add external boundaries if needed | `Services/` or `Services//` when several | | 11 | Add shared domain selectors if needed | `Redux/Selector//` | -| 12 | Build the feature UI | `View//` | +| 12 | Build the feature UI | `View/` or `View//` when several are familiar | | 13 | Mirror tests | `Tests/` | Skip components that provide no value. A state-only transition needs no middleware; a screen-only projection does not need a Redux selector. @@ -252,7 +291,7 @@ Skip components that provide no value. A state-only transition needs no middlewa - Selector tests provide state and assert the derived domain result. - Middleware tests inject mocks, execute an action, and assert the returned follow-up action. - Service tests exercise the external boundary without involving views. -- View-state projection tests live under the matching `Tests/View//` folder, or the consumer-mapped test root. +- Presentation-model tests live under the matching `Tests/Model//` folder, or the consumer-mapped test root. - Test mocks and fixture data live under the test target's `Mocks/` folder. Follow [Unit testing](../Testing/UnitTesting.md) for framework and concurrency conventions. diff --git a/AgentGuidelines/Guidelines/Development.md b/AgentGuidelines/Guidelines/Development.md index afce49f..d572c0e 100644 --- a/AgentGuidelines/Guidelines/Development.md +++ b/AgentGuidelines/Guidelines/Development.md @@ -4,10 +4,30 @@ 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. + ## 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. +## Repository automation + +Use Swift for new repository-owned executable scripts in Swift-focused applications, games, and packages. Prefer the Swift standard library and Foundation so the automation uses the same native toolchain and dependency policy as the codebase. Do not introduce Python, Ruby, JavaScript, or another scripting-language runtime for new validation, transformation, migration, or maintenance logic. + +An existing non-Swift script may remain only as a narrow, documented exception; its existence does not authorize new non-Swift automation. The central `Scripts/swift_format.sh` command wrapper is the retained exception for invoking Xcode's `swift-format` modes. + ## 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: @@ -19,6 +39,14 @@ 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. + ## Logging Applications own their orchestration, lifecycle, and product-domain diagnostics. Follow the shared [logging guide](Logging.md) and rely on each dependency to log its own implementation. Do not duplicate or reformat package-internal operations in the application log. diff --git a/AgentGuidelines/Guidelines/Documentation.md b/AgentGuidelines/Guidelines/Documentation.md index 2a0d327..2d8325c 100644 --- a/AgentGuidelines/Guidelines/Documentation.md +++ b/AgentGuidelines/Guidelines/Documentation.md @@ -1,12 +1,47 @@ # 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 structs, classes, enums, protocols, actors, and other significant types with focused `///` DocC comments. +- Document every new struct, class, enum, protocol, actor, and function with focused `///` DocC comments. +- Use `// MARK: -` pragmas to separate meaningful logical sections so source files remain easy to scan and navigate. - Update documentation when changing a documented API, parameter, behavior, or invariant. - End documentation sentences with periods. - Explain intent, contracts, units, side effects, isolation, and non-obvious constraints; do not restate syntax. @@ -16,9 +51,10 @@ ## Project-level documentation - Keep durable architecture and cross-cutting guides in the consumer's declared documentation folder. -- Update a guide when a change alters the documented architecture, data flow, public API, persistence, navigation, localization process, testing workflow, or delivery workflow. -- Do not update broad guides for minor implementation changes already explained by code and DocC. -- Remove or rewrite stale documentation when its feature or workflow is removed. +- 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. @@ -26,13 +62,16 @@ When reviewing a change, ask: -- Does it alter a documented public API or invariant? +- 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? -Flag missing documentation only when the change affects durable knowledge. Avoid documentation churn for small fixes. +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 diff --git a/AgentGuidelines/Guidelines/Git/Repositories.md b/AgentGuidelines/Guidelines/Git/Repositories.md index bff7933..04e66ce 100644 --- a/AgentGuidelines/Guidelines/Git/Repositories.md +++ b/AgentGuidelines/Guidelines/Git/Repositories.md @@ -40,3 +40,13 @@ When `gh` authentication appears inconsistent: 5. Use SSH for Git transport only when the CLI remains unavailable after retry and the operation is specifically a Git fetch, commit, or push. Continue using `gh` for GitHub API operations whenever it is working. An environment mismatch is not evidence that the user's GitHub account or token is invalid. Record the failed command and exact non-secret error, retry after the authentication check, and report the blocker only after repeated attempts fail. + +### Login-shell credentials + +Some developer environments export `GITHUB_TOKEN` from a shell startup file rather than from the non-interactive process that launched the agent. When the user has explicitly authorized using that local configuration, retry `gh` in a login shell that sources the user's startup configuration: + +```sh +zsh -lc 'source "$HOME/.zshrc"; gh auth status' +``` + +Run the required `gh` operation in that same shell after authentication succeeds. Never print, inspect, copy, or persist the token value; suppress unrelated startup output when practical, and do not source a startup file merely to bypass a credential or permission boundary without the user's authorization. diff --git a/AgentGuidelines/Guidelines/GitHub/PullRequests.md b/AgentGuidelines/Guidelines/GitHub/PullRequests.md index 3535d36..48b85df 100644 --- a/AgentGuidelines/Guidelines/GitHub/PullRequests.md +++ b/AgentGuidelines/Guidelines/GitHub/PullRequests.md @@ -5,51 +5,106 @@ Use this guide whenever creating, reviewing, updating, or merging a GitHub pull ## 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. Inspect all review summaries, inline threads, checks, and requested changes. -3. Assess each comment on its technical merits. -4. Implement valid feedback and rerun the affected validation. -5. If feedback should not be implemented, reply in the original thread with a concise technical reason. -6. Reply to implemented feedback with what changed and where. -7. Resolve a thread only after its concern has been addressed or explicitly declined. -8. After addressing review comments, update the pull-request description so it matches the current implementation, validation, and any remaining limitations. -9. Recheck the pull request immediately before merge for late comments and check-state changes. +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 +PR opened at stable head | v -Codex adds eyes reaction +One automatic full review | - +--> thumbs-up ----------------> Clean review + +--> thumbs-up ----------------> No P0/P1 blockers | - `--> Review comments ----------> Assess each comment - | - fix or decline with reason + `--> Review comments ----------> Classify and group | - reply in original thread + batch P0/P1 fixes | - resolve thread + owner-authorized delta review? + | | + no yes + | | + stop one verification pass + | + no unresolved P0/P1 + | + stop ``` When using the GitHub CLI, monitor all three surfaces: @@ -96,20 +151,26 @@ gh api graphql --paginate \ -F thread= ``` -Continue polling while actively working on the pull request. 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. +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 actionable review comment is unanswered; -- a review conversation is unresolved; +- 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. -If a review arrives after a premature merge, treat that as a process failure: assess the feedback, reply to every thread, and ship valid corrections through a follow-up pull request. +## 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 diff --git a/AgentGuidelines/Guidelines/Localization.md b/AgentGuidelines/Guidelines/Localization.md new file mode 100644 index 0000000..274ca1a --- /dev/null +++ b/AgentGuidelines/Guidelines/Localization.md @@ -0,0 +1,92 @@ +# 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. + +## 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/Packages.md b/AgentGuidelines/Guidelines/Packages.md index da332c3..c5a27d0 100644 --- a/AgentGuidelines/Guidelines/Packages.md +++ b/AgentGuidelines/Guidelines/Packages.md @@ -10,6 +10,29 @@ Start a new ThatFactory project or package README with a centered HTML badge blo

``` +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. @@ -17,11 +40,12 @@ Use only badges that describe the repository, in this order: 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. Updated date. -7. Revision or latest release. -8. License. -9. CI. -10. Release, publishing, or documentation status when applicable. +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. @@ -37,6 +61,7 @@ The common package baseline is Swift, Xcode, Platforms, License, and CI. Add opt - 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. @@ -62,6 +87,8 @@ DocC is the default documentation format for public Swift packages. Document pub 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. diff --git a/AgentGuidelines/Guidelines/Swift/Localization.md b/AgentGuidelines/Guidelines/Swift/Localization.md deleted file mode 100644 index 02725c1..0000000 --- a/AgentGuidelines/Guidelines/Swift/Localization.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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 locations, key conventions, and generated-symbol policy locally. - -## 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. - -## 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. - -## 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 or update source-language content and translator context. -4. Update only the languages in scope. -5. Build to validate catalog syntax, extraction, generated symbols, and bundle lookup. -6. Use previews or runtime visual verification for truncation, layout direction, and formatting when relevant. - -Do not invent translations from an unrelated project's conventions. Product vocabulary and tone remain consumer-specific. diff --git a/AgentGuidelines/Guidelines/Swift/Swift.md b/AgentGuidelines/Guidelines/Swift/Swift.md index 2f6305e..17a29f1 100644 --- a/AgentGuidelines/Guidelines/Swift/Swift.md +++ b/AgentGuidelines/Guidelines/Swift/Swift.md @@ -7,6 +7,7 @@ - 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 @@ -22,9 +23,11 @@ ## State and isolation - Treat actor isolation as part of an API's contract. -- Mark UI-bound reference models `@MainActor` unless the target's default actor isolation already provides it. -- Avoid adding `@MainActor` to tests or domain types merely to silence a diagnostic. Resolve the actual isolation boundary. +- When application and test targets use MainActor default isolation, infer isolated conformances, and `nonisolated(nonsending)` by default, omit annotations that merely restate those effective settings. Verify every affected target before removing annotations. +- `nonisolated(nonsending)` by default governs how nonisolated asynchronous functions run; it does not make synchronous types or conformances nonisolated. Keep explicit `nonisolated` where a value conformance must satisfy a `Sendable` generic contract, a synchronous API is called from a `@Sendable` closure, or another compiler-verified actor boundary requires it. +- Keep an explicit isolation annotation when a declaration intentionally differs from the target default, crosses an actor boundary, belongs to reusable code compiled under different defaults, or implements a documented compiler workaround. - Use `Sendable` where values cross concurrency domains and their stored values support it. +- Avoid adding `@MainActor` to tests or domain types merely to silence a diagnostic. Resolve the actual isolation boundary. ## C-family interoperability diff --git a/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/SwiftLint.md b/AgentGuidelines/Guidelines/Swift/SwiftLint.md deleted file mode 100644 index c37477b..0000000 --- a/AgentGuidelines/Guidelines/Swift/SwiftLint.md +++ /dev/null @@ -1,8 +0,0 @@ -# SwiftLint - -- Treat lint rules as readability and correctness tools, not as architecture. -- Fix warnings introduced by a change. -- Do not add enum namespaces, empty wrapper types, or other artificial structures solely to satisfy filename rules for pure-function files such as reducers, selectors, or middleware. -- Prefer a focused local disable with a short reason when a rule conflicts with the intended design. -- Do not disable a rule repository-wide to avoid fixing one occurrence. -- Keep the lint configuration aligned with the physical folder organization and generated-file exclusions of the consumer repository. diff --git a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md b/AgentGuidelines/Guidelines/Swift/SwiftStyle.md index 49a5aed..6277537 100644 --- a/AgentGuidelines/Guidelines/Swift/SwiftStyle.md +++ b/AgentGuidelines/Guidelines/Swift/SwiftStyle.md @@ -8,9 +8,14 @@ - 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. -- Prefer one primary type or concern per file. +- 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: @@ -23,3 +28,30 @@ 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 index 4b39758..82b7cbb 100644 --- a/AgentGuidelines/Guidelines/Swift/SwiftUI.md +++ b/AgentGuidelines/Guidelines/Swift/SwiftUI.md @@ -4,8 +4,10 @@ Use official Apple documentation and Xcode's current SwiftUI skills for API-spec ## View structure +- Put SwiftUI dynamic properties such as `@Environment`, `@Query`, `@State`, and `@Binding` before ordinary stored `let` and `var` properties. Keep injected environment dependencies before locally owned state when both are present. - Keep a parent view focused on composition. - Model meaningful sections such as headers, lists, metadata, sidebars, and footers as separate `View` types with narrow inputs. +- Keep each independently meaningful `View` in its own file, including private supporting views. Give every view its own deterministic preview when the required dependencies can be represented safely; when they cannot, document the concrete limitation in the handoff. - Do not extract sections into computed `some View` properties merely to shorten `body`; computed properties remain in the parent's invalidation boundary. - Tiny fragments reused within one body may use a small helper when they have no independent state, input, or invalidation story. - Keep view initializers cheap. Do not decode data, access files, build large structures, or allocate formatters in `init`. @@ -58,4 +60,4 @@ With SDKs where `@State` is a macro, do not give a state property a declaration ## Localization -Follow [Localization](Localization.md) for user-facing text, layout direction, formatting, and package bundles. +Follow [Localization](../Localization.md) for user-facing text, layout direction, formatting, and package bundles. diff --git a/AgentGuidelines/Guidelines/Xcode/ProjectSettings.md b/AgentGuidelines/Guidelines/Xcode/ProjectSettings.md new file mode 100644 index 0000000..72ede2d --- /dev/null +++ b/AgentGuidelines/Guidelines/Xcode/ProjectSettings.md @@ -0,0 +1,72 @@ +# 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` + +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. + +## 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 unit-test and UI-test targets, and every supported configuration. Use Xcode project-aware tooling or `xcodebuild -showBuildSettings` to verify the effective values. +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. diff --git a/AgentGuidelines/README.md b/AgentGuidelines/README.md index 7a801a1..f6522f6 100644 --- a/AgentGuidelines/README.md +++ b/AgentGuidelines/README.md @@ -1,17 +1,18 @@

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

# Agent Guidelines -`agent-guidelines` is ThatFactory's public, versioned source of truth for reusable instructions given to coding agents. It centralizes stable decisions about Swift development, Redux architecture, testing, documentation, logging, packages, CI/CD, localization, and Xcode tooling while leaving product context and exceptions in each consuming repository. +`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, not a Swift product. Consumers install a tagged release as a Git subtree at `AgentGuidelines/`, so every agent sees ordinary version-controlled files at predictable paths. +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 @@ -25,7 +26,7 @@ The repository contains documentation, not a Swift product. Consumers install a git subtree add/pull | v -+---------------- Consumer project or package ----------------+ ++---------------- Consumer project or package -----------------+ | | | AGENTS.md | | |-- local product/package context | @@ -35,11 +36,12 @@ The repository contains documentation, not a Swift product. Consumers install a | | | | AgentGuidelines/ | | | |-- VERSION | | +| |-- Configurations/ | | | `-- Guidelines/ <----------------------------------+ | -| |-- Architecture/Redux.md | -| |-- Swift/SwiftUI.md | -| |-- Testing/UnitTesting.md | -| `-- Xcode/MCP.md | +| |-- Architecture/Redux.md | +| |-- Swift/SwiftUI.md | +| |-- Testing/UnitTesting.md | +| `-- Xcode/MCP.md | | | | Sources and project files | +----------------------------+---------------------------------+ @@ -58,24 +60,26 @@ The subtree does not automatically import every guide into an agent's context. A ## Guideline catalog +- [Agent workflow and tool execution](Guidelines/AgentWorkflow.md) +- [CI/CD](Guidelines/CICD.md) +- [Development and reusability](Guidelines/Development.md) +- [Documentation](Guidelines/Documentation.md) +- [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) +- [GitHub pull requests](Guidelines/GitHub/PullRequests.md) +- [Localization](Guidelines/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) -- [SwiftLint](Guidelines/Swift/SwiftLint.md) -- [Localization](Guidelines/Swift/Localization.md) - [Unit and integration testing](Guidelines/Testing/UnitTesting.md) -- [Documentation](Guidelines/Documentation.md) -- [Logging](Guidelines/Logging.md) -- [Swift packages](Guidelines/Packages.md) -- [Development and reusability](Guidelines/Development.md) -- [CI/CD](Guidelines/CICD.md) -- [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) -- [GitHub pull requests](Guidelines/GitHub/PullRequests.md) - [Xcode MCP and visual verification](Guidelines/Xcode/MCP.md) +- [Xcode project settings](Guidelines/Xcode/ProjectSettings.md) - [Xcode security audits](Guidelines/Xcode/Security.md) -Only reference the guides that apply. A UI-agnostic package normally uses Swift, style, testing, documentation, logging, packages, CI/CD, and Xcode guidance, but not Redux or SwiftUI guidance. +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 @@ -85,10 +89,17 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.9 \ + 0.0.27 \ --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 @@ -96,7 +107,33 @@ Keep the subtree tracked, but add this to the consumer's tracked `.gitattributes 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. +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, and external-dependency contract directly in the repository-root `AGENTS.md`; Markdown links to shared guides are navigation, not automatic instruction includes. + +### 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, and External Dependency 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 @@ -106,11 +143,11 @@ Review the target release's changelog, then pull it deliberately: git subtree pull \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.9 \ + 0.0.27 \ --squash ``` -Confirm `AgentGuidelines/VERSION`, ensure the `.gitattributes` rule above is present, review the subtree diff, validate local `AGENTS.md` pointers, and run the consumer's relevant tests. Keep the subtree update in its own commit, and identify the old and new versions plus the central release or pull request in the consumer pull-request description. Updates are intentionally not automatic: one guideline release cannot silently change every project. +Confirm `AgentGuidelines/VERSION`, review the subtree diff, synchronize the marked code-review contract, documentation-maintenance contract, and external-dependency 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 @@ -123,7 +160,7 @@ Confirm `AgentGuidelines/VERSION`, ensure the `.gitattributes` rule above is pre 2. Compare relevant guidance with this repository and official Apple documentation. 3. Bring over durable policy, not the exported skill text or an SDK API catalog. 4. Remove obsolete or conflicting rules instead of accumulating historical alternatives. -5. Run `python3 Scripts/validate_guidelines.py`. +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. 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..cf6a9a5 --- /dev/null +++ b/AgentGuidelines/Scripts/validate_consumer_setup.swift @@ -0,0 +1,388 @@ +#!/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 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*$"# + +/// 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 } +} + +/// 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.py b/AgentGuidelines/Scripts/validate_guidelines.py deleted file mode 100644 index b816c50..0000000 --- a/AgentGuidelines/Scripts/validate_guidelines.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the structure and public safety of the guideline repository.""" - -from __future__ import annotations - -import re -import sys -from pathlib import Path, PurePosixPath - - -ROOT = Path(__file__).resolve().parents[1] -README = ROOT / "README.md" -VERSION = ROOT / "VERSION" -CHANGELOG = ROOT / "CHANGELOG.md" - -MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") -SEMVER = re.compile( - r"^(0|[1-9][0-9]*)\." - r"(0|[1-9][0-9]*)\." - r"(0|[1-9][0-9]*)" - r"(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" - r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?" - r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$" -) -FORBIDDEN = { - "/" + "Users" + "/": "personal absolute path", - "file" + "://": "local file URL", - "mobile-ios-" + "chauffeur": "work-repository identifier", - "black" + "lane": "work-repository identifier", -} - - -def text_files() -> list[Path]: - suffixes = {".md", ".py", ".yml", ".yaml", ".txt"} - files = [path for path in ROOT.rglob("*") if path.is_file() and path.suffix in suffixes] - files.extend(path for path in (ROOT / "VERSION", ROOT / "LICENSE") if path.is_file()) - return sorted(set(files)) - - -def resolve_link(source: Path, raw_target: str) -> Path | None: - target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] - if not target or target.startswith(("#", "http://", "https://", "mailto:")): - return None - - parts = PurePosixPath(target).parts - if "AgentGuidelines" in parts: - index = parts.index("AgentGuidelines") - return ROOT.joinpath(*parts[index + 1 :]).resolve() - - return (source.parent / target).resolve() - - -def validate_links(errors: list[str]) -> None: - for source in sorted(ROOT.rglob("*.md")): - for raw_target in MARKDOWN_LINK.findall(source.read_text(encoding="utf-8")): - resolved = resolve_link(source, raw_target) - if resolved is not None and not resolved.exists(): - relative_source = source.relative_to(ROOT) - errors.append(f"{relative_source}: missing link target {raw_target!r}") - - -def validate_catalog(errors: list[str]) -> None: - readme = README.read_text(encoding="utf-8") - for guide in sorted((ROOT / "Guidelines").rglob("*.md")): - relative = guide.relative_to(ROOT).as_posix() - if f"]({relative})" not in readme: - errors.append(f"README.md: guideline is not cataloged: {relative}") - - -def validate_version(errors: list[str]) -> None: - version = VERSION.read_text(encoding="utf-8").strip() - if not SEMVER.fullmatch(version): - errors.append(f"VERSION: invalid semantic version {version!r}") - - changelog = CHANGELOG.read_text(encoding="utf-8") - if f"## [{version}]" not in changelog: - errors.append(f"CHANGELOG.md: missing release heading for {version}") - - -def validate_readme_contract(errors: list[str]) -> None: - readme = README.read_text(encoding="utf-8") - required = { - 'alt="Xcode"': "Xcode badge alt text", - "thatfactory/agent-guidelines/actions/workflows/ci.yml": "CI badge repository", - "--prefix=AgentGuidelines": "subtree destination", - "https://github.com/thatfactory/agent-guidelines.git": "subtree remote", - "git subtree add": "subtree installation command", - "git subtree pull": "subtree update command", - "AgentGuidelines/** linguist-generated": "generated subtree attribute", - } - for value, description in required.items(): - if value not in readme: - errors.append(f"README.md: missing {description}: {value!r}") - - -def validate_public_content(errors: list[str]) -> None: - for path in text_files(): - contents = path.read_text(encoding="utf-8") - relative = path.relative_to(ROOT) - for forbidden, description in FORBIDDEN.items(): - if forbidden.lower() in contents.lower(): - errors.append(f"{relative}: contains {description}: {forbidden!r}") - - -def main() -> int: - errors: list[str] = [] - validate_links(errors) - validate_catalog(errors) - validate_version(errors) - validate_readme_contract(errors) - validate_public_content(errors) - - if errors: - print("Guideline validation failed:") - for error in errors: - print(f"- {error}") - return 1 - - guide_count = len(list((ROOT / "Guidelines").rglob("*.md"))) - print(f"Validated {guide_count} guidelines for version {VERSION.read_text().strip()}.") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/AgentGuidelines/Scripts/validate_guidelines.swift b/AgentGuidelines/Scripts/validate_guidelines.swift new file mode 100755 index 0000000..54403be --- /dev/null +++ b/AgentGuidelines/Scripts/validate_guidelines.swift @@ -0,0 +1,660 @@ +#!/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 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 documentationGuideline = root.appendingPathComponent("Guidelines/Documentation.md") +let packagesGuideline = root.appendingPathComponent("Guidelines/Packages.md") +let agentsTemplate = root.appendingPathComponent("Templates/AGENTS.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", +] + +/// 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", + ] + 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", + "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", + ] + 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 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", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Development.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", + ] + 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", + "## 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", + "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", + ] + for (value, description) in required where !contents.contains(value) { + errors.append(".agents/skills/agent-guidelines-audit/SKILL.md: missing \(description): '\(value)'") + } + 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", + "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)") + } + } +} + +/// 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) + validateLocalizationScripts(&errors) + validateXcodeProjectSettingsGuideline(&errors) + validateExternalDependencyPolicy(&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/AGENTS.md b/AgentGuidelines/Templates/AGENTS.md index 4bad2bd..96c6873 100644 --- a/AgentGuidelines/Templates/AGENTS.md +++ b/AgentGuidelines/Templates/AGENTS.md @@ -8,32 +8,85 @@ Describe the product or package, supported platforms, and durable constraints. L Read only the guides relevant to the task: +- [Agent workflow](AgentGuidelines/Guidelines/AgentWorkflow.md) - [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) - [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) - [SwiftUI](AgentGuidelines/Guidelines/Swift/SwiftUI.md) -- [SwiftLint](AgentGuidelines/Guidelines/Swift/SwiftLint.md) -- [Localization](AgentGuidelines/Guidelines/Swift/Localization.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). -Add the following section to the consumer repository's root `AGENTS.md` so it is loaded for root-level Codex and pull-request work: +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. ``` -This tracked, synchronized subtree is reviewed centrally in `thatfactory/agent-guidelines`; the root-level instruction ensures the review scope is loaded even when Codex starts from the repository root. +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 @@ -47,6 +100,10 @@ Replace these examples with exact repository paths: | 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..6babf39 --- /dev/null +++ b/AgentGuidelines/Tests/run_tests.swift @@ -0,0 +1,519 @@ +#!/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 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) + } + } + ), + ( + "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 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/Tests/test_validate_guidelines.py b/AgentGuidelines/Tests/test_validate_guidelines.py deleted file mode 100644 index c17e130..0000000 --- a/AgentGuidelines/Tests/test_validate_guidelines.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Tests for the guideline repository validator.""" - -from __future__ import annotations - -import importlib.util -import unittest -from pathlib import Path - - -VALIDATOR_PATH = Path(__file__).resolve().parents[1] / "Scripts" / "validate_guidelines.py" -SPEC = importlib.util.spec_from_file_location("validate_guidelines", VALIDATOR_PATH) -assert SPEC is not None -assert SPEC.loader is not None -VALIDATOR = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(VALIDATOR) - - -class SemanticVersionTests(unittest.TestCase): - """Verifies the supported Semantic Versioning grammar.""" - - def test_valid_versions(self) -> None: - """Accepts core, prerelease, and build metadata forms.""" - versions = ( - "0.0.2", - "1.2.3-rc.1+build.5", - "1.0.0-alpha-beta", - "1.0.0+001", - ) - - for version in versions: - with self.subTest(version=version): - self.assertIsNotNone(VALIDATOR.SEMVER.fullmatch(version)) - - def test_invalid_versions(self) -> None: - """Rejects leading zeroes and incomplete identifiers.""" - versions = ( - "01.2.3", - "1.02.3", - "1.2.03", - "1.2.3-01", - "1.2.3-rc.01", - "1.2.3+", - "1.2.3-", - ) - - for version in versions: - with self.subTest(version=version): - self.assertIsNone(VALIDATOR.SEMVER.fullmatch(version)) - - -if __name__ == "__main__": - unittest.main() diff --git a/AgentGuidelines/VERSION b/AgentGuidelines/VERSION index c5d54ec..24ff855 100644 --- a/AgentGuidelines/VERSION +++ b/AgentGuidelines/VERSION @@ -1 +1 @@ -0.0.9 +0.0.27 From 8d5054df1d41c61d3d1ef4cc90a3deeb5287ea0a Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Wed, 9 Sep 2026 01:42:51 +0200 Subject: [PATCH 4/4] Adopt guideline audit contracts and shared Swift formatting --- .agents/skills/agent-guidelines-audit | 1 + .editorconfig | 1 + .github/workflows/ci.yml | 11 ++++ .swift-format | 1 + AGENTS.md | 54 +++++++++++++++++++ CHANGELOG.md | 4 ++ Package.swift | 4 +- Sources/AppLogger/AppLogger.swift | 6 +-- Sources/Extensions/Date+Formatting.swift | 5 +- .../Extensions/TimeInterval+Formatting.swift | 9 ++-- Tests/AppLoggerTests/AppLoggerTests.swift | 1 + .../AppLoggerTests/DateFormattingTests.swift | 17 ++++-- .../TimeIntervalFormattingTests.swift | 1 + Tests/AppLoggerTests/XCTestManifests.swift | 10 ++-- Tests/LinuxMain.swift | 5 +- 15 files changed, 105 insertions(+), 25 deletions(-) create mode 120000 .agents/skills/agent-guidelines-audit create mode 120000 .editorconfig create mode 120000 .swift-format 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd6d684..c8a0864 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,17 @@ concurrency: cancel-in-progress: true jobs: + swift-format: + name: Swift Format + runs-on: [self-hosted, macOS] + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + - 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] 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 index 2067bf4..4c4644f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,9 @@ AppLogger is a Swift package wrapping Apple's unified logging APIs and providing Read the relevant shared guides before changing the package: - [Swift](AgentGuidelines/Guidelines/Swift/Swift.md) +- [Agent workflow](AgentGuidelines/Guidelines/AgentWorkflow.md) +- [Development workflow](AgentGuidelines/Guidelines/Development.md) +- [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) - [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) - [Unit and integration testing](AgentGuidelines/Guidelines/Testing/UnitTesting.md) - [Documentation](AgentGuidelines/Guidelines/Documentation.md) @@ -23,3 +26,54 @@ Keep the package UI-agnostic and preserve its public API's minimal scope. ## 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. + + + +## 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. + + + + +## 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. + + + + +## 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). + + +## Local compatibility and validation + +AppLogger requires Swift tools `6.3.3` for Xcode Cloud compatibility. Keep this explicit compatibility requirement when applying the shared toolchain guidance. The README Xcode badge records the locally verified Xcode version. + +CI runner labels are `self-hosted` and `macOS`, with a selected Xcode toolchain supporting the package manifest. PR and main validation share `.github/workflows/ci.yml` intentionally to keep identical checks in one workflow. Run the shared formatter over `Package.swift`, `Sources`, and `Tests`, followed by `swift test`. CI uses non-mutating strict lint and validates the guideline consumer integration. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d32f7b..6f7e535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project are documented in this file. ## [1.1.1] - 2026-09-09 +### Changed + +- Updated AgentGuidelines from `0.0.9` to `0.0.27`, adopted its consumer contracts and audit integration, and added shared Swift formatting validation to CI. + ### Fixed - Lowered the minimum Swift tools version from `6.4` to `6.3.3` so Xcode Cloud environments using Swift `6.3.3` can resolve the package. Public APIs and platform requirements are unchanged. diff --git a/Package.swift b/Package.swift index 5501adf..58c57eb 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( .iOS(.v26), .macOS(.v26), .tvOS(.v26), - .watchOS(.v26) + .watchOS(.v26), ], products: [ .library( @@ -28,6 +28,6 @@ let package = Package( .testTarget( name: "AppLoggerTests", dependencies: ["AppLogger"] - ) + ), ] ) diff --git a/Sources/AppLogger/AppLogger.swift b/Sources/AppLogger/AppLogger.swift index d227ec1..ea15751 100644 --- a/Sources/AppLogger/AppLogger.swift +++ b/Sources/AppLogger/AppLogger.swift @@ -9,7 +9,6 @@ import os /// - [OSLog](https://developer.apple.com/documentation/os/oslog) /// - [Logger](https://developer.apple.com/documentation/os/logger) public struct AppLogger { - // MARK: - Properties /// Default values used by the `AppLogger`. @@ -42,8 +41,7 @@ public struct AppLogger { // MARK: - Interface -public extension AppLogger { - +extension AppLogger { /// Logs a string interpolation at the given level. /// /// - Parameters: @@ -51,7 +49,7 @@ public extension AppLogger { /// - message: The `String` to be logged. /// - isPrivate: Sets the `OSLogPrivacy` to be used by the function. `true` means `.private`; /// `false` means `.public`. The default is `false`. - func log(level: AppLogLevel = Defaults.level, _ message: String, isPrivate: Bool = Defaults.isPrivate) { + public func log(level: AppLogLevel = Defaults.level, _ message: String, isPrivate: Bool = Defaults.isPrivate) { if isPrivate { logger.log(level: level.osLogType, "\(message, privacy: .private)") } else { diff --git a/Sources/Extensions/Date+Formatting.swift b/Sources/Extensions/Date+Formatting.swift index 716fbd9..55f545f 100644 --- a/Sources/Extensions/Date+Formatting.swift +++ b/Sources/Extensions/Date+Formatting.swift @@ -1,9 +1,8 @@ import Foundation -public extension Date { +extension Date { /// Formats the date as `dd/MM HH:mm:ss` for concise logging output. - nonisolated - func formattedLogTimestamp( + public nonisolated func formattedLogTimestamp( locale: Locale = .autoupdatingCurrent, timeZone: TimeZone = .autoupdatingCurrent ) -> String { diff --git a/Sources/Extensions/TimeInterval+Formatting.swift b/Sources/Extensions/TimeInterval+Formatting.swift index e053144..b98eee4 100644 --- a/Sources/Extensions/TimeInterval+Formatting.swift +++ b/Sources/Extensions/TimeInterval+Formatting.swift @@ -1,10 +1,11 @@ import Foundation -public extension TimeInterval { +extension TimeInterval { /// Formats a duration for concise logging output. - nonisolated - func formattedLogDuration() -> String { - guard isFinite else { return "0s" } + public nonisolated func formattedLogDuration() -> String { + guard isFinite else { + return "0s" + } // Leave headroom because Double(Int.max) rounds to an out-of-range value. let maxRepresentableSeconds = Double(Int.max) - 1024 diff --git a/Tests/AppLoggerTests/AppLoggerTests.swift b/Tests/AppLoggerTests/AppLoggerTests.swift index 70ea145..d4431c1 100644 --- a/Tests/AppLoggerTests/AppLoggerTests.swift +++ b/Tests/AppLoggerTests/AppLoggerTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import AppLogger final class AppLoggerTests: XCTestCase { diff --git a/Tests/AppLoggerTests/DateFormattingTests.swift b/Tests/AppLoggerTests/DateFormattingTests.swift index df296ca..3c1f42d 100644 --- a/Tests/AppLoggerTests/DateFormattingTests.swift +++ b/Tests/AppLoggerTests/DateFormattingTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing + @testable import AppLogger /// Verifies log-friendly timestamp formatting. @@ -7,16 +8,24 @@ import Testing @Test func formatsDateAsDayMonthAndTime() { let calendar = Calendar(identifier: .gregorian) - let components = DateComponents(calendar: calendar, timeZone: TimeZone(secondsFromGMT: 0), year: 2026, month: 5, day: 12, hour: 21, minute: 15, second: 4) + let components = DateComponents( + calendar: calendar, timeZone: TimeZone(secondsFromGMT: 0), year: 2026, month: 5, day: 12, hour: 21, + minute: 15, second: 4) let date = components.date ?? .distantPast - #expect(date.formattedLogTimestamp(locale: Locale(identifier: "en_GB"), timeZone: TimeZone(secondsFromGMT: 0) ?? .autoupdatingCurrent) == "12/05 21:15:04") + #expect( + date.formattedLogTimestamp( + locale: Locale(identifier: "en_GB"), timeZone: TimeZone(secondsFromGMT: 0) ?? .autoupdatingCurrent) + == "12/05 21:15:04") } @Test func preservesDayMonthOrderForMonthFirstLocales() { - let date = Date(timeIntervalSince1970: 1778620504) + let date = Date(timeIntervalSince1970: 1_778_620_504) - #expect(date.formattedLogTimestamp(locale: Locale(identifier: "en_US"), timeZone: TimeZone(secondsFromGMT: 0) ?? .autoupdatingCurrent) == "12/05 21:15:04") + #expect( + date.formattedLogTimestamp( + locale: Locale(identifier: "en_US"), timeZone: TimeZone(secondsFromGMT: 0) ?? .autoupdatingCurrent) + == "12/05 21:15:04") } } diff --git a/Tests/AppLoggerTests/TimeIntervalFormattingTests.swift b/Tests/AppLoggerTests/TimeIntervalFormattingTests.swift index 971d7b2..4bdedaf 100644 --- a/Tests/AppLoggerTests/TimeIntervalFormattingTests.swift +++ b/Tests/AppLoggerTests/TimeIntervalFormattingTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing + @testable import AppLogger /// Verifies log-friendly duration formatting. diff --git a/Tests/AppLoggerTests/XCTestManifests.swift b/Tests/AppLoggerTests/XCTestManifests.swift index ecef454..f362ad1 100644 --- a/Tests/AppLoggerTests/XCTestManifests.swift +++ b/Tests/AppLoggerTests/XCTestManifests.swift @@ -1,9 +1,9 @@ import XCTest #if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(AppLoggerTests.allTests), - ] -} + public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(AppLoggerTests.allTests) + ] + } #endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 6f4a1f4..f90b413 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,7 +1,6 @@ -import XCTest - import AppLoggerTests +import XCTest -var tests = [XCTestCaseEntry]() +var tests: [XCTestCaseEntry] = [] tests += AppLoggerTests.allTests() XCTMain(tests)