From 5da2712c6cbf257258641cb31f00b0fd8b8fe866 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Sat, 19 Sep 2026 15:44:37 +0200 Subject: [PATCH 1/2] Update Agent Guidelines to 0.0.33 --- .../skills/agent-guidelines-audit/SKILL.md | 88 +- .../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/.gitignore | 63 ++ AgentGuidelines/AGENTS.md | 16 +- AgentGuidelines/CHANGELOG.md | 136 +++ AgentGuidelines/Guidelines/AppStore.md | 38 + AgentGuidelines/Guidelines/CICD.md | 41 + AgentGuidelines/Guidelines/Development.md | 26 +- AgentGuidelines/Guidelines/Documentation.md | 48 +- AgentGuidelines/Guidelines/Git/IgnoreFiles.md | 15 + .../Guidelines/GitHub/PullRequests.md | 34 +- AgentGuidelines/Guidelines/Localization.md | 94 ++ AgentGuidelines/Guidelines/Logging.md | 2 + AgentGuidelines/Guidelines/Packages.md | 70 +- .../Guidelines/Swift/Localization.md | 43 - AgentGuidelines/Guidelines/Swift/Swift.md | 1 + AgentGuidelines/Guidelines/Swift/SwiftUI.md | 2 +- .../Guidelines/Xcode/ProjectSettings.md | 80 ++ AgentGuidelines/README.md | 34 +- .../Scripts/prepare_localizable_symbols.swift | 236 +++++ .../Scripts/validate_consumer_setup.py | 304 ------ .../Scripts/validate_consumer_setup.swift | 465 +++++++++ .../Scripts/validate_guidelines.py | 349 ------- .../Scripts/validate_guidelines.swift | 830 ++++++++++++++++ .../Scripts/validate_string_catalogs.swift | 421 ++++++++ AgentGuidelines/Templates/.gitignore | 66 ++ AgentGuidelines/Templates/AGENTS.md | 59 +- AgentGuidelines/Tests/run_tests.swift | 911 ++++++++++++++++++ .../Tests/test_validate_consumer_setup.py | 253 ----- .../Tests/test_validate_guidelines.py | 144 --- AgentGuidelines/VERSION | 2 +- 34 files changed, 4255 insertions(+), 1149 deletions(-) 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/Guidelines/AppStore.md create mode 100644 AgentGuidelines/Guidelines/Git/IgnoreFiles.md create mode 100644 AgentGuidelines/Guidelines/Localization.md delete mode 100644 AgentGuidelines/Guidelines/Swift/Localization.md create mode 100644 AgentGuidelines/Guidelines/Xcode/ProjectSettings.md create mode 100755 AgentGuidelines/Scripts/prepare_localizable_symbols.swift delete mode 100755 AgentGuidelines/Scripts/validate_consumer_setup.py 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/.gitignore create mode 100755 AgentGuidelines/Tests/run_tests.swift delete mode 100644 AgentGuidelines/Tests/test_validate_consumer_setup.py 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 index 2110942..75510ae 100644 --- a/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md +++ b/AgentGuidelines/.agents/skills/agent-guidelines-audit/SKILL.md @@ -14,7 +14,8 @@ Perform a final, evidence-based compliance pass. Treat the applicable guidelines 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 `python3 AgentGuidelines/Scripts/validate_consumer_setup.py` 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. +6. When the repository contains an `AgentGuidelines/` subtree, run `AgentGuidelines/Scripts/validate_consumer_setup.swift` from the consumer root. The validator detects Swift-format adoption from the root `AGENTS.md`; add `--require-swift-format` only when the repository must adopt it before that link is present. Treat failures as integration drift to fix or report before handoff. +7. Audit the root `.gitignore` against the shared [Git ignore guide](../../../Guidelines/Git/IgnoreFiles.md) and [`Templates/.gitignore`](../../../Templates/.gitignore). Compare active patterns rather than comments, blank lines, section order, or duplicates. When implementation is authorized, add every missing non-conflicting shared pattern using the template spelling, then verify its effect with `git check-ignore -v` and inspect matching tracked files with `git ls-files`; do not untrack files merely because a new rule matches them. Preserve rule order and negation semantics, and report a conflict instead of silently changing behavior. Treat an active pattern absent from the template as project-specific: accept it only when an adjacent `# Project-specific: ` comment contains a concrete non-placeholder repository-owner-approved rationale. Otherwise report the exact extra pattern and ask the repository owner to choose between updating agent-guidelines or documenting it locally; do not remove, rewrite, or silently approve it. A missing root `.gitignore`, an unresolved missing shared pattern, or an undecided undocumented extra pattern blocks audit completion. Do not inspect or require the user's global Codex instructions. They are user-level state outside the repository audit boundary; validate the checked-in root `AGENTS.md` contract instead. @@ -28,10 +29,86 @@ Review the actual change rather than only checking whether files exist: - Check that framework objects, persistence, logging, and asynchronous work remain in their allowed boundaries. - Check SwiftUI composition, narrow inputs, local versus durable state, localization, accessibility, and safe deterministic previews where applicable. - Check tests for the required framework, mirrored paths, shared tags, Given/When/Then structure, deterministic seams, and coverage of changed behavior and failure paths. -- Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, and noise controls when logging changed. -- Check durable documentation, package configuration, CI/CD, Xcode project configuration, security-sensitive changes, and physical-device limitations when they are in scope. Compare documented Swift and concurrency settings with the effective application and test-target settings; flag both redundant isolation annotations and missing annotations at compiler-verified boundaries. +- Trace every new or changed stateful, asynchronous, fallible, or lifecycle-oriented behavior and verify that its owning artifact emits privacy-safe AppLogger events for the meaningful success, failure, cancellation, recovery, and state-transition outcomes needed to diagnose it. Dependency declaration and target linkage alone do not establish logging coverage. Accept silence for pure values or utilities only when there is no meaningful event boundary and the implementation handoff records that deliberate decision. +- Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, noise controls, and focused formatter or sink tests when logging changed. +- For every Apple-platform application or Swift package in scope, except the AppLogger provider repository itself, verify integration with the shared [Logging guide](../../../Guidelines/Logging.md): confirm the AppLogger dependency is declared, the `AppLogger` library product is linked to every target that emits diagnostics, and any new project has it available in its primary runtime target before its first log call. Search the actual package or Xcode dependency graph rather than relying on an `import` alone, and treat `print`, direct `Logger` instances, or duplicate logging backends as incomplete integration when they emit project diagnostics. When implementation is authorized, add or repair the dependency and target linkage and migrate affected calls while preserving the guide's ownership, subsystem, category, emoji, privacy, severity, and noise rules; report an exact blocker when target or platform constraints make safe integration ambiguous. +- Inspect dependency manifests, resolver or lock files, Xcode package references, vendored source or binary frameworks, and equivalent dependency declarations. Compare the change with the baseline and identify every new third-party dependency or expansion of an existing third-party dependency into a new target or runtime role. Apply the shared [external dependency policy](../../../Guidelines/Development.md#external-dependencies): require explicit repository-owner approval before the dependency is introduced and require the durable exception record in repository documentation. Do not infer approval merely from an execution plan, pull-request description, implementation convenience, package popularity, or the dependency already appearing in the diff. Treat an unapproved or undocumented third-party dependency as a blocker to completion. Do not flag Apple system frameworks, the Swift standard library, ThatFactory-owned packages, or guideline-mandated tooling used only for its documented tooling role. If a newly resolved transitive third-party package will be linked into or shipped with the product, verify that its owning direct dependency is covered by an approved exception rather than dismissing it solely because it is transitive. +- Search dependency manifests, generated directories, project files, and documentation for CocoaPods or Carthage adoption. The shared [external dependency policy](../../../Guidelines/Development.md#external-dependencies) forbids both without an exception path and requires Swift Package Manager for package dependencies. When implementation is authorized, remove newly introduced adoption and its generated or configuration files; report pre-existing adoption as a completion blocker when safe migration is outside the task scope. +- Check package configuration, CI/CD, Xcode project configuration, security-sensitive changes, and physical-device limitations when they are in scope. For every Xcode project, perform the project-settings audit below. Compare documented Swift and concurrency settings with the effective application and test-target settings; flag both redundant isolation annotations and missing annotations at compiler-verified boundaries. - Search for stale type names, superseded files, direct APIs forbidden by the new architecture, empty folders, and references to removed behavior. -- For pull-request or merge readiness, apply the root `## Code Review Rules`: confirm the review covers the current head, no allowed review round is pending, every 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. +- Apply the shared [CI/CD guide](../../../Guidelines/CICD.md) to workflow and repository-automation changes. Search scripts, generated directories, workflow files, and documentation for fastlane adoption and treat it as forbidden without an exception path. Require focused ThatFactory tooling or repository-owned Swift scripts for CI/CD and delivery, and inspect new repository-owned executable scripts for compliance. For Swift-focused applications, games, and packages, accept Python or POSIX shell only when durable repository documentation identifies the missing Swift capability, exact script and task scope, runtime and dependency requirements, security and maintenance impact, validation method, and revisit or removal condition. Reject convenience, familiarity, shorter code, an existing interpreter, or another non-Swift script as justification. Treat every exception as narrow; the central `Scripts/swift_format.sh` wrapper is the retained documented exception for invoking Xcode's `swift-format` modes. +- For pull-request or merge readiness, apply the root `## Code Review Rules`: confirm the Codex review covers the current head, no allowed Codex review round is pending, every Codex review thread has a disposition, and no unresolved P0/P1 blocker remains. Treat P2/P3 observations as non-blocking and never request another Codex review unless the repository owner explicitly authorizes it. This Codex review-round budget does not apply to otherwise-authorized Reasoning Relay/ChatGPT review delegations; do not block them waiting for a Codex-budget exception. + +## Audit Xcode project settings + +For every checked-in `.xcodeproj`, read and apply the shared [Xcode project-settings guide](../../../Guidelines/Xcode/ProjectSettings.md): + +1. Identify the selected Xcode and its newest stable Swift language mode. Inspect every `PBXProject` build configuration and project-level `.xcconfig`; target-only values do not satisfy project-level ownership. +2. Require `GCC_TREAT_WARNINGS_AS_ERRORS`, `MTL_TREAT_WARNINGS_AS_ERRORS`, and `SWIFT_TREAT_WARNINGS_AS_ERRORS` to be `YES`; require `SWIFT_APPROACHABLE_CONCURRENCY = YES`, `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`, and `SWIFT_STRICT_CONCURRENCY = complete`; require `SWIFT_VERSION` to select that newest stable language mode; and require application targets to inherit `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO` in every configuration. +3. Discover every build setting exposed by the active Xcode whose name begins with `SWIFT_UPCOMING_FEATURE_`. For the selected Swift language mode, use the setting documentation and compiler diagnostics to identify which features remain opt-in. Require those settings to be `YES` at project level, and require flags for features already incorporated into the language mode to be absent so warnings-as-errors cannot turn a redundant-feature diagnostic into a build failure. Treat the settings listed in the shared guide as the Xcode 27 discovery inventory, not as flags that must all be enabled and not as a future-exhaustive list. +4. Enumerate every target and configuration, including application, unit-test, and UI-test targets, and inspect effective values with Xcode project-aware tooling or `xcodebuild -showBuildSettings`. For every application target, inspect the built `Info.plist` and confirm the `ITSAppUsesNonExemptEncryption` Boolean is `NO`. Remove redundant target copies and language-mode-redundant upcoming-feature flags. Treat a disabling or different target override for an applicable baseline setting as a violation unless an exact exception applies. +5. Before reporting a failure, search the nearest applicable `AGENTS.md` and durable project documentation linked from it for an exception naming the exact setting, scope, concrete incompatibility, replacement, impact, validation, and revisit condition. Accept and report an applicable documented exception; do not infer one from transient discussion, generic project prose, or the existing build setting itself. An application may use `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = YES` only when that exact exception documents the shipped non-exempt cryptography and resulting export-compliance workflow. +6. When implementation is authorized, move or add compliant values at project level, remove redundant target copies, and rerun build-setting inspection plus relevant builds. For review-only work, report undocumented gaps without editing. + +## Audit Swift package settings + +Enumerate every checked-in `Package.swift` that belongs to repository source, ignoring `.build` and other generated build artifacts, and treat each manifest's directory as a package root. For every package, read and apply [the shared package compiler-settings baseline](../../../Guidelines/Packages.md#compiler-settings-baseline); do not infer package rules solely from the Xcode guide. + +1. Identify the selected Swift/Xcode toolchain, inspect `// swift-tools-version:` and `swiftLanguageModes`, and require the newest stable language mode from the shared baseline, currently Swift 6. Reject an older effective mode unless an exact documented exception applies. +2. Run `swift package --package-path dump-package`, or the selected-toolchain equivalent, and use the evaluated manifest to enumerate locally defined targets and their effective settings. Do not rely only on textual grep because manifests may construct or mutate settings programmatically. +3. For every locally defined target that compiles Swift and for which SwiftPM exposes `swiftSettings`, including production, test, and other applicable Swift target kinds, require unconditional `.treatAllWarnings(as: .error)` plus `ExistentialAny`, `InferIsolatedConformances`, `InternalImportsByDefault`, `MemberImportVisibility`, and `NonisolatedNonsendingByDefault` as `.enableUpcomingFeature(...)` settings. Do not require Swift settings on binary targets, system-library targets, or package plug-in targets because `Target.plugin(...)` does not expose `swiftSettings`. +4. For locally compiled C or Objective-C targets, require the applicable typed `CSetting.treatAllWarnings(as: .error)`; require the corresponding `CXXSetting` where C++ is compiled. Do not require C-family settings when those languages are absent, and do not accept `unsafeFlags` in place of available typed APIs. +5. In Swift 6 mode, treat complete strict concurrency as supplied by the language mode and require every redundant explicit `StrictConcurrency` opt-in to be removed, including `.enableUpcomingFeature("StrictConcurrency")`, `.enableExperimentalFeature("StrictConcurrency")`, and `StrictConcurrency=complete` representations found in the evaluated manifest or effective compiler arguments. Require other upcoming-feature declarations to be removed when they become unconditional in the selected language mode, especially because warnings-as-errors can promote the resulting diagnostics. +6. Do not require or automatically add `.defaultIsolation(MainActor.self)`. Its absence is compliant with the shared package baseline; source-level actor-isolation review remains a separate Swift and concurrency concern. +7. Search the nearest applicable `AGENTS.md` and linked durable documentation before reporting a package-setting failure. Accept only an exception that names the exact setting or feature, package and targets, incompatibility, replacement or omission, impact, compensating validation where applicable, and revisit condition. Do not infer an exception from the manifest. +8. Run the package's documented validation workflow and, at minimum where applicable, `swift build` and `swift test`. Warnings promoted to errors must leave both warning-clean. When conditions or helper logic make the effective invocation uncertain, use verbose SwiftPM output to verify `-warnings-as-errors`, the language mode, and every `-enable-upcoming-feature` argument. + +For authorized implementation work, repair the manifest and rerun evaluated-manifest inspection plus builds. For review-only work, report the exact missing, conditional, redundant, or conflicting setting without modifying the package. + +## Audit localization + +When the repository contains localized targets or String Catalogs, read the shared [Localization guide](../../../Guidelines/Localization.md) and the consumer's local translation guidance: + +1. Keep supported languages, catalog and source paths, product voice, glossary, non-translatable terms, and project-specific exceptions in consumer documentation. Do not move those specifics into shared guidance or infer them from another product. +2. Confirm the project uses generated localizable symbols for maintained Swift catalog entries, does not check generated Swift into source control, and does not add localizable Swift literals that bypass the generated API. +3. Require the synchronized `prepare_localizable_symbols.swift` and `validate_string_catalogs.swift` logic. A local wrapper may supply project paths and languages to preserve a stable developer or CI command, but it must not retain a forked copy of shared migration or validation logic. +4. Run the consumer's documented nonmutating preparation check and catalog validator. Confirm CI runs the validator for localized projects and that every configured catalog and Swift source root is covered. +5. Inspect stale entries, required-language coverage, translation states, plural variants, and format placeholders in context. Require source/translated-language, long-text, plural, and right-to-left verification when affected. +6. Establish the explicit Git base for the completed change. If any added, copied, modified, renamed, or untracked `.xcstrings` file exists relative to that base, open every changed catalog in Xcode and inspect its String Catalog editor diagnostics. Record the repository-relative catalog path, selected Xcode version and build, and an explicit result of zero editor errors and zero editor warnings for each catalog. +7. From the consumer root, run `AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift --base-ref --inspected-catalog --evidence-output `, repeating `--inspected-catalog` for every changed catalog. In this source repository, use `.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift`. Keep the JSON evidence outside the repository and summarize its catalog paths, Xcode build, and zero-diagnostic results in the completion handoff and pull-request description. +8. Fail closed when a changed catalog lacks that recorded editor evidence. A catalog validator, `xcstringstool`, warning-clean build, test run, or unrecorded statement that Xcode was checked is not a substitute. Do not claim audit success, open or update a pull request, or declare merge readiness until the evidence gate passes. +9. Preserve machine-translation state until fluent review. Treat missing required validation, unresolved catalog errors or warnings, missing catalog-editor evidence, or undocumented project-specific deviations as incomplete implementation. + +## Audit App Store metadata + +When a repository contains an `AppStore/` directory or the change affects storefront content, read the shared [App Store metadata guide](../../../Guidelines/AppStore.md) and the consumer's product-specific App Store documentation: + +1. Confirm the repository remains the source of truth and follows the app-store-connect-mcp directory format. Keep the app identity, platform versions, locales, product copy, screenshot production, reviewer prerequisites, and export-compliance rationale in consumer documentation. +2. Verify that changed fields and files match the requested domains and locales. Treat omitted fields as unmanaged, preserve curated content outside scope, and apply the consumer's localization voice and terminology to storefront copy. +3. Require scope-matched `validate_repository` and immutable plan evidence for any requested remote synchronization. Inspect additions, changes, removals, destructive modes, operation IDs, plan identity, and digest before application. +4. After an authorized apply, inspect uncertain outcomes through `get_operation_status` without replaying writes, then plan the unchanged scope again and require `noOp: true` remote reconciliation. +5. Confirm `.appstore-connect-mcp/` is ignored and that repository content, diffs, logs, and evidence contain no credentials, private contacts, demo secrets, upload URLs, or operational journals. +6. Treat App Store submission and release as separately authorized operations. Ordinary metadata or asset synchronization never proves release readiness and never authorizes submission. + +## Audit documentation consistency + +When implementation, configuration, or workflow behavior changed, perform an explicit documentation-drift pass: + +1. Read the applicable [Documentation guide](../../../Guidelines/Documentation.md) and identify code-level or durable project documentation that describes the affected feature, API, configuration, workflow, or invariant. +2. Compare those documented claims with the final implementation. Require a documentation update when the change alters durable or core behavior, or when any existing documented claim becomes inaccurate, incomplete, misleading, or obsolete, regardless of change size. +3. Search relevant durable documentation for changed names, removed behavior, defaults, examples, diagrams, setup steps, and references. Inspect matches in context rather than assuming a keyword search alone proves consistency. +4. Do not require new project-level prose for incidental implementation details that are not durable and do not affect an existing documented claim. +5. When implementation is authorized, update or remove stale documentation in the same change. For review-only work, report the drift without editing. Known stale documentation blocks completion. + +## Audit documentation formatting + +Apply the conventions in the applicable [Documentation guide](../../../Guidelines/Documentation.md) to governed Markdown: + +1. Audit every added or changed Markdown file outside a synchronized, provenance-verified `AgentGuidelines/` subtree. +2. When the completed change adds or changes a documentation convention, or updates a consumer to a guideline release that does so, also audit the consumer's existing root Markdown files and declared durable documentation folders. This adoption pass is required even when those files did not otherwise change. +3. From the repository root, run `.agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift ` against those files or folders. The checker is read-only and reports prose paragraphs, list items, ordinary blockquotes, and GitHub alert body paragraphs that span multiple physical lines while excluding alert marker lines, fenced code, and other common verbatim Markdown constructs. +4. Inspect each reported span in context. Join confirmed hard-wrapped prose so each paragraph, list item, or blockquote occupies one physical line. Preserve intentional structure such as headings, separate list items, tables, fenced code, and ASCII diagrams. +5. When implementation is authorized, fix confirmed violations and rerun the checker. For review-only work, report them without editing. Do not claim the audit passes while a confirmed line-wrapping violation remains in scope. ## Validate the evidence @@ -66,8 +143,11 @@ Summarize: - the instruction and guideline areas audited; - consumer-integration validation and any drift found; +- `.gitignore` reconciliation, including added shared patterns and every accepted or unresolved project-specific entry; - findings fixed during the audit; +- documentation updated or removed, or why no documentation change was required; - validation commands and outcomes; +- changed-String-Catalog detection and, when applicable, the recorded Xcode version, catalog paths, and zero catalog-editor errors and warnings; - any deliberate deviations, unavailable evidence, or remaining blockers. Do not say the work is done merely because the audit ran. Say it is ready only when the requested outcome is complete and the relevant evidence passes. diff --git a/AgentGuidelines/.agents/skills/agent-guidelines-audit/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/.gitignore b/AgentGuidelines/.gitignore index dff2f41..3a3fc11 100644 --- a/AgentGuidelines/.gitignore +++ b/AgentGuidelines/.gitignore @@ -1,3 +1,66 @@ +# macOS .DS_Store + +# Xcode user data, build output, and archives +xcuserdata/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.hmap +/build/ +/DerivedData/ +*.ipa +*.dSYM +*.dSYM.zip + +# Xcode playground generated state +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +/.build/ +/Packages/ +.swiftpm/configuration/registries.json + +# Node.js +node_modules/ +dist/ +coverage/ +*.tgz +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python __pycache__/ *.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Local credentials and environment configuration +.env +.env.* +!.env.example +!.env.*.example +.netrc + +# ThatFactory tooling state +.appstore-connect-mcp/ +.devspace/ +/public-check/ + +# Project-specific entries require a durable rationale after repository-owner review. +# Project-specific: diff --git a/AgentGuidelines/AGENTS.md b/AgentGuidelines/AGENTS.md index f82a61c..36e9089 100644 --- a/AgentGuidelines/AGENTS.md +++ b/AgentGuidelines/AGENTS.md @@ -37,7 +37,13 @@ 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. @@ -50,15 +56,17 @@ Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretica 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. -Automatic Codex review is the initial review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another review after each remediation commit. Within the normal review budget, at most one owner-authorized, delta-scoped verification review may be requested under [the pull-request review workflow](Guidelines/GitHub/PullRequests.md). - +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 1600a67..dcb012e 100644 --- a/AgentGuidelines/CHANGELOG.md +++ b/AgentGuidelines/CHANGELOG.md @@ -2,6 +2,142 @@ All notable changes to this project are documented in this file. +## [0.0.33] - 2026-09-19 + +### Added + +- Added a Swift Package compiler-settings baseline aligned with the applicable Xcode project warning, Swift language-mode, concurrency, and upcoming-feature policies. +- Added completion-audit enforcement for compiler settings in checked-in Swift packages. + +### Changed + +- Required future Xcode compiler-policy changes to evaluate and update Swift Package Manager parity when an equivalent package setting is applicable. + +## [0.0.32] - 2026-09-13 + +### Changed + +- Made runtime observability an explicit consumer contract for changed stateful, asynchronous, fallible, and lifecycle behavior, while preserving silence for pure values and utilities without meaningful diagnostic boundaries. +- Extended the completion audit to require useful privacy-safe AppLogger outcome coverage instead of accepting dependency declaration and target linkage alone. + +## [0.0.31] - 2026-09-13 + +### Added + +- Added a least-privilege GitHub App authentication pattern for workflows that resolve private sibling repositories, including short-lived read-only tokens, process-scoped Git configuration, exact repository selection, and fork pull-request and self-hosted-runner security boundaries. + +## [0.0.30] - 2026-09-12 + +### Changed + +- Prohibited dedicated License headings and conventional standalone license-description paragraphs in Swift package READMEs while retaining root license files and standard license badges. + +## [0.0.29] - 2026-09-12 + +### Added + +- Added a reusable `.gitignore` template for Xcode, Swift Package Manager, and supported development tooling, plus shared guidance for reviewed project-specific exceptions. + +### Changed + +- Extended the completion audit to reconcile consumer `.gitignore` files with the shared template and escalate undocumented extra patterns for a repository-owner decision. +- Prohibited CocoaPods and Carthage under the external-dependency policy, and prohibited fastlane under CI/CD guidance while requiring first-party Swift or ThatFactory delivery tooling and narrowing Python or shell automation to documented Swift capability gaps. + +## [0.0.28] - 2026-09-09 + +### Added + +- Added a shared version-controlled App Store metadata and app-store-connect-mcp synchronization workflow, including scoped planning, immutable application, reconciliation, screenshot, credential, and submission boundaries. +- Added `ITSAppUsesNonExemptEncryption = NO` to the Xcode application baseline and completion audit, with a documented-exception path for apps that ship non-exempt encryption. +- Added mandatory post-merge cleanup of merged local feature branches after returning to an updated primary branch. + +### Changed + +- Linked localization guidance to the shared App Store metadata workflow while keeping product voice, locales, and concrete storefront content in consumer repositories. + +## [0.0.27] - 2026-09-05 + +### Fixed + +- Preserved valid GitHub alert syntax in the Markdown wrapping audit while continuing to reject hard-wrapped alert bodies, ordinary blockquotes, malformed alert markers, and nested alerts. + +## [0.0.26] - 2026-09-02 + +### Added + +- Added a fail-closed completion-audit helper that requires structured Xcode String Catalog editor evidence for every changed `.xcstrings` file. +- Added native Swift tests for guideline, consumer-integration, Markdown, and String Catalog automation. + +### Changed + +- Moved the shared localization guide from `Guidelines/Swift/` to `Guidelines/` and updated repository and consumer-template links. +- Replaced Python validation, localization, and audit-helper scripts with native Swift executables, and moved CI and release validation to macOS runners. +- Required new repository-owned executable scripts in Swift-focused repositories to use Swift, with the existing `swift_format.sh` wrapper retained as a narrow exception. + +## [0.0.25] - 2026-09-02 + +### Added + +- Added generic generated-symbol localization guidance plus reusable String Catalog preparation and validation scripts with consumer-configured paths and languages. +- Added an Xcode project-settings baseline covering warnings-as-errors, strict and approachable concurrency, default MainActor isolation, the latest stable Swift language mode, and every upcoming feature that remains opt-in for that language mode. +- Added documentation conventions for single-line Markdown prose and aligned ASCII diagrams, together with a deterministic wrapping checker. + +### Changed + +- Expanded the completion audit to verify localization workflows, project-level Xcode setting inheritance and documented exceptions, documentation formatting, and convention adoption across existing durable documentation. +- Updated the consumer template and guideline catalog for the new Xcode project-settings and localization workflows. + +## [0.0.24] - 2026-08-31 + +### Added + +- Added a versioned external-dependency contract to consumer `AGENTS.md` files so applications, games, and reusable packages default to native or ThatFactory-owned implementations and require explicit repository-owner approval plus a durable decision record for third-party product dependencies. +- Extended the completion audit and consumer-setup validation to detect unapproved or undocumented dependency additions and contract drift. + +### Changed + +- Clarified package guidance so first-party packages cannot conceal third-party runtime dependencies and guideline-mandated tooling remains tooling-only. + +## [0.0.23] - 2026-08-26 + +### Added + +- Added a versioned documentation-maintenance contract to the consumer `AGENTS.md` template and consumer-setup validation so guideline upgrades detect projects that have not adopted the contract. + +### Changed + +- Strengthened documentation guidance and the completion audit so durable behavior changes and any change that makes existing documentation stale require documentation updates, while incidental implementation details do not create documentation churn. + +## [0.0.22] - 2026-08-24 + +### Added + +- Required completion audits to verify and repair shared AppLogger integration for ThatFactory Apple-platform applications and Swift packages. + +## [0.0.21] - 2026-08-23 + +### Changed + +- Clarified that the bounded review-round policy applies to Codex GitHub reviews, while otherwise-authorized ChatGPT and Reasoning Relay delegations use their own workflow limits. +- Namespaced Codex review tracking fields and advanced the consumer code-review contract to v2. + +## [0.0.20] - 2026-08-21 + +### Changed + +- Documented squash merge as the default for ThatFactory repositories and instructed agents to use `gh pr merge --squash` instead of attempting merge commits. + +## [0.0.19] - 2026-08-19 + +### Added + +- Added a complete README badge-block example covering Swift, Xcode, platform, package manager, agent/tooling, DocC, license, updated, revision, CI, and publishing badges. + +### Changed + +- Updated this repository's README badges to use the canonical order and applicable tooling, release, and maintenance badges. +- Updated the README contract validator to require the canonical `Xcode MCP` badge alt text. + ## [0.0.18] - 2026-08-18 ### Changed diff --git a/AgentGuidelines/Guidelines/AppStore.md b/AgentGuidelines/Guidelines/AppStore.md new file mode 100644 index 0000000..ec5ed3f --- /dev/null +++ b/AgentGuidelines/Guidelines/AppStore.md @@ -0,0 +1,38 @@ +# App Store metadata + +Use a version-controlled `AppStore/` directory as the source of truth for managed App Store Connect content. Edit repository files first, then synchronize them with ThatFactory's [app-store-connect-mcp](https://github.com/thatfactory/app-store-connect-mcp). Do not maintain managed descriptions, promotional text, screenshots, or review notes through independent browser edits. + +## Repository structure + +Follow the MCP's current [AppStore directory format](https://github.com/thatfactory/app-store-connect-mcp/blob/main/Documentation/AppStore-Format.md). Keep structured values in JSON, prose in UTF-8 text files, and reusable screenshot originals under `AppStore/assets/`. One `AppStore/` root represents one App Store app; require an explicit root when a repository contains more than one app. + +Keep product-specific values in the consumer repository, including the Apple ID, bundle identifier, platforms, version strings, storefront locales, categories, URLs, product copy, review instructions, screenshot sets, and any narrower synchronization commands. Storefront locale identifiers and files are distinct from application String Catalogs, but the consumer's documented voice and terminology apply to both. + +Omission means unmanaged. Add only fields and domains the project intends to reconcile. Do not infer missing translations, version values, categories, availability, capabilities, or release choices from another project. + +## Synchronization workflow + +1. Edit only the requested files and fields in `AppStore/`. Preserve curated content outside the requested scope and obtain the project's required translation review for localized copy. +2. Run `validate_repository` with the explicit App Store root, platform, version, locales, and selected domains. Treat validation as scope-aware: unfinished content outside the intended operation must not expand that operation. +3. Run the applicable planning tool with the same scope. Use `plan_metadata_changes` for listing and review metadata, `plan_screenshot_changes` for screenshot sets, and the corresponding dedicated planner for commerce, provisioning, signing, or submission. Inspect the exact app, platform version, locales, operation IDs, additions, changes, removals, and destructive effects. +4. Apply only an inspected immutable plan within the owner's authorized scope. Supply the exact `planId`, digest, operation IDs, and required host confirmation to `apply_plan`. Plans are process-bound, so keep planning and application in the same MCP server process. +5. If an outcome is uncertain, inspect `get_operation_status` and remote state. Never replay a write whose execution may have started. +6. Plan the same scope again against unchanged repository inputs and require `noOp: true`. Record the reconciliation result alongside the repository change. + +Ordinary metadata, screenshot, commerce, provisioning, or signing synchronization does not authorize submitting a version for review or releasing it. Use the MCP's explicit readiness and submission workflow only when the owner has separately placed submission in scope. + +## Screenshots + +Treat each localization's `screenshots.json` as the ordered manifest for its selected display sets. Paths are relative to `AppStore/`, and the explicit array order is the storefront order. Preserve reusable originals in the repository; do not silently resize, transcode, substitute another locale, or derive ordering from filenames. + +Use `merge` when unrelated remote screenshots must remain unmanaged. Use `replace` only when the selected set should match the manifest exactly, and inspect every planned removal before applying it. After upload, allow Apple processing to finish and reconcile the final remote bytes and order. A temporarily absent source checksum is processing-pending evidence, not proof of a mismatch. + +## Credentials and operational state + +Keep `.appstore-connect-mcp/` as an ignored sibling of `AppStore/`; it contains process and recovery state, not repository content. Never commit or print App Store Connect keys, signing keys, certificates, private contacts, demo credentials, upload URLs, environment files, or MCP journals. Represent supported App Review secrets through the MCP's narrow environment-variable references, and pass credential variable names rather than values when configuring an MCP client. + +If the MCP process does not inherit credentials that are available to an authorized shell, correct or deliberately bridge the process environment without exposing values. Do not diagnose a credential as invalid merely because a different process did not inherit it. + +## Project-specific documentation + +Consumer documentation should identify its concrete `AppStore/` paths and managed domains, explain how product voice and locale coverage apply to storefront copy, describe screenshot production, and record any review, export-compliance, or release prerequisites. Keep generic MCP mechanics in this guide and link to them instead of copying the workflow into every repository. diff --git a/AgentGuidelines/Guidelines/CICD.md b/AgentGuidelines/Guidelines/CICD.md index c420331..5138aca 100644 --- a/AgentGuidelines/Guidelines/CICD.md +++ b/AgentGuidelines/Guidelines/CICD.md @@ -12,6 +12,47 @@ - Do not place secrets in workflow files, logs, fixtures, or command arguments that may be echoed. - Keep release workflows separate from pull-request validation when their permissions differ. +## Tooling and automation + +Fastlane is forbidden in every ThatFactory project and has no exception path. Build repository-owned CI/CD and delivery automation with focused ThatFactory tooling such as `xcode-cloud-mcp` and `app-store-connect-mcp`, plus Swift scripts where repository-specific orchestration is needed. + +Use Swift for new repository-owned executable scripts in Swift-focused applications, games, and packages. Prefer the Swift standard library and Foundation so automation uses the same native toolchain and dependency policy as the codebase. Convenience, familiarity, shorter code, or an existing interpreter is not a reason to choose another language. + +Fall back to Python or a POSIX shell script only when the required behavior cannot be implemented with the repository's supported Swift toolchain and Foundation APIs. Document the exception in durable repository documentation in the same change, including the missing Swift capability, exact script and task scope, runtime and dependency requirements, security and maintenance impact, validation method, and condition for revisiting or removing the exception. Keep the fallback narrow; an existing non-Swift script does not authorize another one. The central `Scripts/swift_format.sh` command wrapper is the retained documented exception for invoking Xcode's `swift-format` modes. + +## Private repository dependencies + +The workflow repository's `GITHUB_TOKEN` does not grant access to private dependencies in sibling repositories. When Swift Package Manager or another build tool must clone private ThatFactory repositories, use a GitHub App installed on every required dependency repository. The app does not need access to the workflow repository unless that repository is also an intended token target. Grant the app only read access to repository contents, mint a short-lived installation token with `actions/create-github-app-token`, and list the exact dependency repositories in the action's `repositories` input. Do not use a personal access token, a long-lived machine credential, or an organization-wide token when the GitHub App can provide the required scope. + +Expose the installation token only to steps that resolve or build the private dependencies. Supply HTTPS authentication through Git's process-level `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_0`, and `GIT_CONFIG_VALUE_0` environment variables so the credential is not persisted in repository or global Git configuration. Keep the existing dependency URLs as `https://github.com//` URLs. For example: + +```yaml +- name: Create private dependency token + id: private-dependencies + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.PRIVATE_DEPENDENCIES_APP_CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_DEPENDENCIES_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: | + first-private-package + second-private-package + permission-contents: read + +- name: Test + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: url.https://x-access-token:${{ steps.private-dependencies.outputs.token }}@github.com/.insteadOf + GIT_CONFIG_VALUE_0: https://github.com/ + run: swift test +``` + +The GitHub App's installation and repository selection are part of the security boundary. Consumer documentation must name the app variable and secret, list the private repositories the workflow requires, record the required `Contents: read` permission, and identify the jobs or steps that receive the token. Keep pull-request and protected-branch workflows consistent unless a documented trust boundary requires otherwise. Because `GIT_CONFIG_*` values are ordinary inherited environment variables, treat the credential-bearing resolve or build step and its complete subprocess tree as privileged. Tests, build scripts, SwiftPM plugins, and other code executed beneath that step must be trusted to receive read access to every repository in the token scope. + +Repository secrets are unavailable to workflows triggered by pull requests from forks. A repository that accepts fork-originated or otherwise untrusted pull requests must keep a secretless validation path or deliberately skip private-dependency jobs with an explicit, documented condition. Untrusted code must not execute on a persistent self-hosted runner that is later reused for credential-bearing work. Use an isolated disposable or ephemeral self-hosted runner, an appropriate GitHub-hosted runner where possible, or a separate runner pool or host that never subsequently receives secrets; otherwise skip the untrusted validation. + +This trust rule is event-independent. Do not combine credentials with code that is not trusted at that privilege level under `pull_request`, `pull_request_target`, `issue_comment`, `workflow_run`, or another trigger. On self-hosted runners, do not expose the token to unrelated steps, caches, artifacts, logs, or persistent configuration; retain the action's default post-job token revocation. + ## `ci-pr.yml` Projects using GitHub Actions should keep pull-request validation in `.github/workflows/ci-pr.yml`, triggered by `pull_request` events for `opened`, `synchronize`, and `reopened`. diff --git a/AgentGuidelines/Guidelines/Development.md b/AgentGuidelines/Guidelines/Development.md index c733ca1..0c61a9f 100644 --- a/AgentGuidelines/Guidelines/Development.md +++ b/AgentGuidelines/Guidelines/Development.md @@ -4,6 +4,22 @@ When developing a new feature or responding to a feature request, consider shared code first. If the code fits an existing package, suggest extending that package instead of adding the implementation directly to an application. Also consider whether the change belongs in a new Swift package, even when that package does not exist yet. Prefer reusable, focused package APIs when they can serve more than one consumer. +## External dependencies + +ThatFactory applications, games, and reusable packages are first-party by default. Do not introduce a new third-party source or binary dependency during normal feature development. Prefer Apple platform APIs, the Swift standard library, code owned by the current repository, or a focused ThatFactory-owned package. If reusable capability is missing, implement it natively at the appropriate boundary and consider extracting it into a first-party package when it can serve multiple consumers. + +Do not add a third-party dependency merely to save implementation time, reduce code volume, or avoid learning a platform API. Do not make an external dependency acceptable by hiding it behind a first-party wrapper. + +An exception requires explicit approval from the repository owner for the specific dependency and use before changing the dependency graph. If approved, record the decision in durable repository documentation in the same change, such as the architecture document or an ADR. Record the dependency and source, purpose and target scope, why a native or first-party implementation is not appropriate, relevant license, security, and maintenance considerations, version or update policy when material, and the approval context. Do not infer approval from an execution plan, pull-request description, agent choice, or transient chat that merely mentions or uses the dependency. Regardless of where explicit approval occurs, reflect the exception in durable repository documentation. + +An existing approved dependency does not authorize a different dependency. Expanding an existing third-party dependency to a new target or runtime role requires the same approval and documentation. Routine version updates that do not change the approved role remain governed by the intentional consumer-update workflow in [Packages](Packages.md). + +For this policy, a third-party dependency is externally maintained source or binary code linked into or shipped with the product, including externally maintained Swift packages, vendored libraries, frameworks or XCFrameworks, CocoaPods, Carthage dependencies, and equivalent runtime libraries. Apple system frameworks and the Swift standard library are platform dependencies, not third-party dependencies. Packages maintained by ThatFactory are first-party dependencies. + +Tooling dependencies explicitly required by these shared guidelines, such as documentation or build plugins used only by tooling, are pre-approved for that documented role. They must not be linked into or shipped with product runtime targets unless the repository owner separately approves and documents that use. + +CocoaPods and Carthage are forbidden in every ThatFactory project and are not eligible for the exception process above. Use Swift Package Manager for package dependencies. + ## Guidelines version Before changing a project, verify that it uses the latest released version of `agent-guidelines`. Check the project's `AgentGuidelines/VERSION` against the latest release, update the subtree or equivalent when it is behind, and read the updated applicable guides before starting implementation. This check is manual and must be performed at the beginning of each project task. @@ -25,8 +41,16 @@ Before claiming implementation is complete, handing work to the user, preparing, 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 `python3 AgentGuidelines/Scripts/validate_consumer_setup.py` to detect drift in the root Code Review contract, 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. +For subtree consumers, the audit runs `AgentGuidelines/Scripts/validate_consumer_setup.swift` to detect drift in the root Code Review and Documentation Maintenance contracts, Codex subtree-review scope, `.gitattributes`, local guide links, and repository skill symlink. When the root `AGENTS.md` links the shared Swift-format guide, the validator also requires the shared configuration symlinks and strict non-mutating CI adoption. User-level global Codex instructions are outside this repository audit. + +## Post-merge cleanup + +After an in-scope feature pull request merges, switch the original checkout back to the repository's primary branch, update it from its upstream with a fast-forward-only pull, and delete the merged local feature branch. Confirm the remote feature branch is absent when the repository deletes merged branches automatically. Do this before declaring the feature workflow complete so stale branches do not accumulate. + +Never discard unrelated changes to perform cleanup. If the original checkout is dirty, another worktree still uses the feature branch, the merge did not complete, or the primary branch cannot fast-forward, leave the branch intact and report the exact blocker. Do not use force deletion merely to hide an unmerged branch. ## Logging Applications own their orchestration, lifecycle, and product-domain diagnostics. Follow the shared [logging guide](Logging.md) and rely on each dependency to log its own implementation. Do not duplicate or reformat package-internal operations in the application log. + +Treat observability as part of implementing or changing stateful, asynchronous, fallible, or lifecycle-oriented behavior. Before handoff, trace those boundaries and verify that privacy-safe AppLogger events distinguish the outcomes needed to diagnose the behavior in context. Merely adding the dependency or linking its product is not sufficient. Keep pure value and utility code silent when it has no meaningful event boundary, and record that deliberate decision in the handoff rather than manufacturing noisy logs. diff --git a/AgentGuidelines/Guidelines/Documentation.md b/AgentGuidelines/Guidelines/Documentation.md index 4cc60c5..2d8325c 100644 --- a/AgentGuidelines/Guidelines/Documentation.md +++ b/AgentGuidelines/Guidelines/Documentation.md @@ -1,9 +1,43 @@ # Documentation +Documentation is part of implementation. For every codebase change, evaluate whether durable project knowledge or any existing documentation is affected; do not treat documentation as optional cleanup after code and tests are complete. + +## Conventions + - Use PascalCase Markdown filenames without spaces. - Keep the folder flat until one topic genuinely requires several files. - Prefer current implementation over speculative future design; label known gaps explicitly. +### Line wrapping + +**Write each paragraph as a single physical line — do not hard-wrap prose at a fixed column width.** A paragraph hand-wrapped at ~100 columns shows up with breaks mid-sentence, which looks broken. Let your editor **soft-wrap** instead of inserting newlines. + +The same rule applies to multi-sentence **list items** and **blockquotes** — keep each item/quote on one line. GitHub alerts are the exception: keep the alert marker on its required quoted line and keep each following body paragraph on one physical quoted line. Do not nest alerts. This concerns **prose only**: fenced code blocks and ASCII diagrams are published verbatim, so wrap those exactly as they should appear (one line per row). + +### Diagrams + +Use ASCII art inside fenced code blocks: + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ View │──────>│ Store │──────>│ Service │ +└──────────┘ └──────────┘ └──────────┘ +``` + +Use box-drawing characters (`─`, `│`, `┌`, `┐`, `└`, `┘`) and arrows (`──>`, `<──`, `v`, `^`). + +#### Keep diagrams aligned + +Diagrams are published verbatim, so a diagram that is misaligned in the repo will look misaligned on MD readers/editors. Misalignment is also the most common defect in these files — it creeps in when someone edits a label without re-padding the rest of the row. + +- **Every vertical rule must sit in the same character column on every row.** Decide the column positions up front, then pad each row with spaces to hit them. In a sequence diagram, each participant's lifeline (`│`) is one such column. +- **A box's interior width must match its border width.** `┌──────────┐` (10 dashes) needs exactly 10 characters between the `│` on the rows below it. +- **Put arrowheads beside the target rule, not on top of it** — `│─────>│`, so the lifeline stays unbroken. An arrow that spans intermediate participants simply passes through their columns. +- **When a label is too long for its cell, wrap it onto a second row** rather than letting it push the rules out of alignment. +- **Count characters, not bytes.** Box-drawing glyphs are 3-byte UTF-8, so `wc -c` and `awk '{print length}'` report well above the visual width — around 2× for a typical row, up to 3× for one that is mostly box-drawing. Measure with `swift -e 'import Foundation; let value = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""; print(value.split(separator: "\n", omittingEmptySubsequences: false).map(\.count).max() ?? 0)' < FILE` or an editor's column indicator. + +Check the result in a monospace view before committing: scan down each vertical rule and confirm it never jogs left or right. For a large or heavily edited diagram, it is quicker and safer to generate the block from a list of column positions in a throwaway script than to count spaces by hand. + ## Code-level documentation - Document every new struct, class, enum, protocol, actor, and function with focused `///` DocC comments. @@ -17,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. @@ -27,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/IgnoreFiles.md b/AgentGuidelines/Guidelines/Git/IgnoreFiles.md new file mode 100644 index 0000000..d1f7a26 --- /dev/null +++ b/AgentGuidelines/Guidelines/Git/IgnoreFiles.md @@ -0,0 +1,15 @@ +# Git Ignore Files + +Use the shared [`.gitignore` template](../../Templates/.gitignore) for new Xcode projects and Swift packages. It covers generated Xcode and Swift Package Manager state plus the local output, caches, and credentials used by supported Node.js, Python, App Store, and ThatFactory development tooling. + +The template deliberately does not ignore `Package.resolved`, `.xcodeproj`, or `.xcworkspace` files. Decide whether a package lockfile belongs in source control based on the package's role, and keep authored Xcode project and workspace definitions tracked. Narrow generated files are ignored instead of ignoring containers that may hold shared configuration. + +## Consumer reconciliation + +Compare a consumer's active `.gitignore` patterns with the template without requiring comments, blank lines, section order, or duplicate rules to match. Add missing shared patterns using their template spelling, preserving the consumer's rule order and negation semantics. Before adding a pattern, check whether a later consumer rule negates it or whether it would hide a currently tracked source or configuration path; report a conflict instead of changing behavior silently. + +Treat every active consumer pattern that is not in the template as a project-specific entry. Do not remove it or silently accept it. Report it to the repository owner so they can choose whether the reusable template should gain the pattern or the consumer should retain it as a local exception. + +When the repository owner chooses a local exception, document the rationale immediately above the entry in the consumer `.gitignore` using `# Project-specific: `. A concrete non-placeholder rationale marks that entry as reviewed, so later completion audits may accept it without reporting it again. Keep exception comments and their patterns adjacent; each independently motivated group needs its own rationale. + +Do not copy ignored caches or generated artifacts into the repository merely to make the comparison pass. Existing tracked files remain tracked even when a matching ignore rule is added, so inspect `git ls-files` and `git check-ignore -v` when a rule's effect is uncertain. diff --git a/AgentGuidelines/Guidelines/GitHub/PullRequests.md b/AgentGuidelines/Guidelines/GitHub/PullRequests.md index 80c12b1..48b85df 100644 --- a/AgentGuidelines/Guidelines/GitHub/PullRequests.md +++ b/AgentGuidelines/Guidelines/GitHub/PullRequests.md @@ -48,33 +48,35 @@ When replying with a commit reference, write the commit hash as raw text without 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. -### Review state and round budget +### Codex review state and round budget -Track enough state to prevent duplicate requests and unbounded review loops: +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 -initial_review_sha -last_reviewed_sha -review_requested_sha -review_round -pending_review -unresolved_p0 -unresolved_p1 -deferred_findings +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 review is the one initial full review. Do not request another review after each fix. A repository owner may explicitly authorize at most one delta-scoped verification review after the known P0/P1 findings have been batch-remediated. +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 request, verify that no Codex review is pending, no existing request targets the current head SHA, the current head differs from `last_reviewed_sha`, and the verification-round budget is unused. Persist `review_requested_sha`, increment `review_round`, and mark `pending_review` before waiting for a result so a retry cannot submit a duplicate request. +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 verification request explicitly: +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 review or restart a full review without separate, explicit repository-owner authorization and a named unresolved P0/P1 concern. A new finding in verification must be a P0/P1 defect introduced by the remediation or genuinely hidden by the previous blocker. +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. @@ -151,6 +153,10 @@ gh api graphql --paginate \ 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: diff --git a/AgentGuidelines/Guidelines/Localization.md b/AgentGuidelines/Guidelines/Localization.md new file mode 100644 index 0000000..4cb53f1 --- /dev/null +++ b/AgentGuidelines/Guidelines/Localization.md @@ -0,0 +1,94 @@ +# Localization + +Follow Apple's [Localizing your app using agents](https://developer.apple.com/documentation/xcode/localizing-your-app-using-agents) workflow and current Xcode localization tools. Consumer repositories declare their supported languages, catalog and source locations, product voice, terminology, and narrow exceptions locally. + +Storefront metadata uses separate App Store locale identifiers and files. Follow [App Store metadata](AppStore.md) for the repository structure and App Store Connect synchronization workflow; apply the consumer's local voice and product terminology to both application and storefront copy. + +## Source artifacts + +- Use the consumer's existing String Catalogs (`.xcstrings`) as the source of truth. +- Do not create a parallel catalog or migrate an existing `.strings` setup unless the task includes that migration. +- An app target uses its main bundle by default. Swift packages and frameworks must resolve localized resources from their own bundle, using the current Apple-recommended bundle API. +- Keep one source of truth for translator context: either the source comment or the catalog comment. + +## Generated symbols + +- Define user-facing text in the appropriate String Catalog first and use Xcode-generated `LocalizedStringResource` symbols from Swift. Enable Generate String Catalog Symbols when an older project does not already generate them. +- Give a string a semantic catalog key when deriving a readable symbol from its source value would be ambiguous. Name formatted variables in the source value so Xcode generates labeled parameters. +- Use generated properties and functions directly in SwiftUI, and resolve them with `String(localized:)` or `AttributedString(localized:)` only where that concrete value is required. +- Generated Swift is build output. Inspect it through Xcode or `xcstringstool` when useful, but never edit or check it in. +- Do not add new localizable Swift literals after a project adopts generated symbols. Use `Text(verbatim:)` only for nonlinguistic punctuation, identifiers, and other intentionally unlocalized content. + +## User-facing values + +- Let SwiftUI's localized string initializers preserve localization context. +- Use `LocalizedStringResource` when a model, view state, notification, or other non-view value carries user-facing text that should resolve later. +- Use `String(localized:)` when a resolved localized `String` is genuinely required outside SwiftUI. +- Use `Text(verbatim:)` for intentional non-localized literals such as debug identifiers. +- Do not pass a runtime `String` to a localized initializer and expect Xcode to extract it as a catalog key. + +## Sentences and formatting + +- Interpolate values into one localizable sentence rather than concatenating translated fragments. +- Add translator comments for ambiguous language and describe interpolated placeholders by position and meaning. +- Use locale-aware `FormatStyle` APIs for dates, numbers, lists, measurements, and currencies. +- Avoid runtime case transformations for localized interface text; allow translations to choose appropriate casing. +- Keep placeholder positions, semantic names, and conversion types identical across source and translated variants. +- Add language-specific plural variants for counts instead of branching between singular and plural text in Swift. +- Preserve the distinction between unavailable data and numeric zero. + +## Layout + +- Use leading and trailing instead of left and right for directional layout. +- Avoid fixed text frames that cannot accommodate translation length or script height. +- Prefer semantic text styles to fixed point sizes. +- Use the SwiftUI environment locale for view behavior that must respond to preview or subtree locale overrides. + +## Agent workflow + +1. Inspect the consumer's local localization instructions and catalogs. +2. Ask Xcode's current documentation or localization capability for the supported workflow. +3. Add the key, source-language value, and translator comment directly to the catalog. Mark an explicitly maintained generated-symbol entry as manual and use named placeholders for formatted values. +4. Inspect the generated API through Xcode's catalog inspector or, when needed, `xcrun xcstringstool generate-symbols`. Use the generated property or function from Swift. +5. For a legacy catalog whose keys were extracted from Swift literals, run the shared preparation script once. It preserves comments, variants, and translations, leaves stale entries for an explicit decision, and refuses to invent a missing source value for an already-manual semantic key. Use `--check` for a nonmutating readiness check. +6. Use Xcode's localization coordinator and String Catalog tools to add or update only the languages in scope. Do not replace contextual translation decisions with ad hoc JSON-rewriting scripts. +7. Resolve every stale extracted entry and every active translated value marked `new` or `needs_review`. Keep agent output machine-translated until a fluent reviewer approves it. +8. Run the shared catalog validator after every catalog or localization-source change. It checks generated-symbol readiness, stale entries, required languages, translation states, format signatures, and Swift literals that bypass generated symbols. +9. Open each changed catalog in Xcode and require zero catalog-editor errors or warnings; these diagnostics do not necessarily become compiler warnings. Record the inspected catalog paths, selected Xcode version and build, and explicit zero-error and zero-warning result. Automated catalog validation, a warning-clean build, or an unrecorded visual check does not satisfy this editor-evidence gate. +10. Build and test source and translated languages. Exercise long strings, every plural branch, and right-to-left layout even when no right-to-left locale ships. +11. Have a fluent reviewer inspect machine translations before recording them as reviewed. + +## Shared scripts + +Supply project-specific paths and supported languages at the consumer boundary: + +```sh +AgentGuidelines/Scripts/prepare_localizable_symbols.swift \ + \ + --check + +AgentGuidelines/Scripts/validate_string_catalogs.swift \ + --catalog-directory \ + --source-directory \ + --required-language +``` + +Repeat directory, catalog, or language options when the project has several. A consumer may keep a small repository-owned wrapper so existing developer and CI commands supply its paths and languages consistently; the wrapper must delegate to the synchronized shared script rather than copy its validation or migration logic. + +The completion audit discovers changed String Catalogs relative to an explicit Git base and fails closed until every catalog has a recorded Xcode editor inspection. After opening each changed catalog and confirming zero editor errors and warnings, run the audit helper from the consumer root and keep its JSON outside the repository: + +```sh +AgentGuidelines/.agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift \ + --base-ref \ + --inspected-catalog \ + --evidence-output +``` + +Repeat `--inspected-catalog` for every changed catalog. The helper records the selected Xcode version and build together with the zero-diagnostic result. Summarize that record in the completion handoff and pull-request description; do not commit the temporary JSON evidence. + +Do not invent translations from an unrelated project's conventions. Product vocabulary and tone remain consumer-specific. + +## References + +- [Using generated localizable symbols in your code](https://developer.apple.com/documentation/xcode/using-generated-localizable-symbols-in-your-code) +- [Localizing your app using agents](https://developer.apple.com/documentation/xcode/localizing-your-app-using-agents) diff --git a/AgentGuidelines/Guidelines/Logging.md b/AgentGuidelines/Guidelines/Logging.md index 498c356..4cf3bb3 100644 --- a/AgentGuidelines/Guidelines/Logging.md +++ b/AgentGuidelines/Guidelines/Logging.md @@ -8,6 +8,7 @@ Use this guide for Apple-platform applications and Swift packages that emit runt - A consuming application logs its own orchestration and lifecycle events. It must not reproduce or reformat a dependency's internal steps or outcomes. - A reusable package describes events using its own domain language. Do not introduce concepts from one current client into package categories or messages. - Ownership does not require every API or package to emit logs. Pure utilities and operations without a meaningful diagnostic event may emit nothing. +- New or changed stateful, asynchronous, fallible, or lifecycle-oriented behavior must identify its meaningful diagnostic boundaries during implementation. Emit a privacy-safe outcome for success and each operationally distinct failure, cancellation, recovery, or state transition that a developer needs to distinguish. If an artifact is limited to pure values or utilities and has no such boundary, keep it silent and state that decision in the implementation handoff instead of adding initializer or property-access noise. - Logging is a side effect. It must not affect returned values, state transitions, error handling, or control flow. - Architectures that isolate side effects must call a logging package from an allowed side-effect boundary, such as middleware or a service, rather than from a pure reducer. @@ -56,6 +57,7 @@ Use this guide for Apple-platform applications and Swift packages that emit runt - Verify that every package message starts with its canonical emoji. - Verify the stable category, meaningful fields, privacy choice, log level, and single-emission behavior for each logged operation. - Do not make tests depend on querying the operating system's persisted log store. +- For every changed lifecycle covered by the observability requirement, test the event formatter or sink for its successful and operationally distinct unsuccessful outcomes. Dependency declaration and target linkage alone do not establish logging coverage. ## Filtering diff --git a/AgentGuidelines/Guidelines/Packages.md b/AgentGuidelines/Guidelines/Packages.md index a9b0576..c33cfbb 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. @@ -30,7 +53,8 @@ The common package baseline is Swift, Xcode, Platforms, License, and CI. Add opt - Use descriptive `alt` text. Preserve a repository's established Xcode badge convention when the label intentionally records the last verified Xcode version. - Prefer dynamic Updated and Revision badges backed by repository history or releases so maintainers do not edit dates and versions by hand. - Do not advertise a platform, integration, package manager, or agent that the repository does not support. -- Keep the repository's license in a root `LICENSE` file when reuse or redistribution is permitted. A README license heading is optional; the badge is a summary, not the license grant. +- Keep the repository's license in a root `LICENSE` file when reuse or redistribution is permitted. +- Retain the README license badge when the standard badge set applies, but do not add a dedicated License heading or license-description section to a package README. GitHub already presents the repository license beside the README, and the badge provides the summary without duplicating license prose. - Do not add a license to an existing repository without the owner's explicit choice of terms. ## Package boundaries @@ -38,12 +62,54 @@ 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. - Put sources under `Sources//` and tests under `Tests/Tests/`. - Keep resources in the target that owns them and use the package bundle for lookup. +## Compiler settings baseline + +Host Xcode build settings are not a substitute for package configuration. Each package must express its applicable compiler policy in `Package.swift` so independently invoked SwiftPM builds, Xcode builds, and dependency builds receive the same strictness. This baseline is the SwiftPM representation of the applicable compiler policy in [Xcode project settings](Xcode/ProjectSettings.md), not a mechanical copy of Xcode build-setting names. + +New packages must use the newest supported Swift tools version. Every package must use the newest stable Swift language mode supported by the selected toolchain, currently Swift 6, preferably owned once at package level: + +```swift +swiftLanguageModes: [.v6] +``` + +Do not repeat `.swiftLanguageMode(.v6)` target by target when package-level ownership is sufficient. A target may specialize the language mode only under a documented package exception. An older manifest that cannot express this baseline must first modernize its tools version; `.treatAllWarnings(as:)` requires PackageDescription 6.2 or later. + +Every locally defined target that compiles Swift and for which SwiftPM exposes `swiftSettings`, including test targets and other applicable Swift target kinds, must unconditionally use the typed PackageDescription API: + +```swift +swiftSettings: [ + .treatAllWarnings(as: .error), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), +] +``` + +A setting restricted only to Debug, Release, a platform, or another build condition does not satisfy this package-wide baseline unless a documented exception covers that scope. Do not use `unsafeFlags` for warning handling when the typed API is available. + +When a locally compiled target contains C or Objective-C, require `cSettings: [.treatAllWarnings(as: .error)]`. Where C++ settings apply, require `cxxSettings: [.treatAllWarnings(as: .error)]`. Pure-Swift packages do not need C or C++ settings, and packages must not introduce `-Werror` through `unsafeFlags` when the typed APIs are available. + +Swift 6 language mode enables complete concurrency checking unconditionally, so a Swift 6 package must not retain any explicit `StrictConcurrency` opt-in. Remove `.enableUpcomingFeature("StrictConcurrency")`, `.enableExperimentalFeature("StrictConcurrency")`, and `StrictConcurrency=complete` spellings instead of treating one representation as special. For an older package, modernize to the required language mode instead of preserving the legacy mode with a compatibility flag. + +Do not invent a direct SwiftPM equivalent for `SWIFT_APPROACHABLE_CONCURRENCY`. Its applicable opt-in language behavior is represented by the individual upcoming features above, including `InferIsolatedConformances` and `NonisolatedNonsendingByDefault`. + +`.defaultIsolation(MainActor.self)` is intentionally not part of this shared package baseline. Reusable packages must express actor isolation according to their public and internal API semantics rather than inherit an application's default isolation policy. A package may choose a default isolation as an intentional package-specific architectural decision, but the completion audit must not add or require it merely for Xcode-project parity. + +Require the listed upcoming features on every applicable locally defined Swift target, but not on binary or system-library targets that SwiftPM does not compile as Swift source or package plug-in targets for which `Target.plugin(...)` does not expose `swiftSettings`. Reevaluate the list whenever the selected Xcode/Swift toolchain or language mode changes. When a feature becomes unconditional in the selected language mode, remove its `.enableUpcomingFeature(...)` declaration from packages, remove it from this baseline, and update the audit contract in the same change. Retain no redundant upcoming features merely for historical consistency because they can produce diagnostics under warnings-as-errors. + +Metal warnings-as-errors and application `Info.plist` export-compliance declarations have no package equivalent in this baseline. SwiftPM has no first-class Metal warning setting, and a reusable package does not own its consuming application's generated `Info.plist`. + +Accept a deviation only when the nearest applicable `AGENTS.md`, or durable documentation linked from it, records the exact package compiler setting or feature, affected package and targets, concrete incompatibility, replacement or omission, engineering impact, compensating validation where applicable, and condition for revisiting or removing the exception. Do not infer an exception from the existing `Package.swift`. + ## Logging Packages own any diagnostics emitted by their implementation. Follow the shared [logging guide](Logging.md) for AppLogger usage, subsystem identity, package emoji prefixes, domain-owned categories, concise messages, privacy, and test coverage. A consuming application must not reproduce package-internal logs. @@ -63,6 +129,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 6ae523d..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 diff --git a/AgentGuidelines/Guidelines/Swift/SwiftUI.md b/AgentGuidelines/Guidelines/Swift/SwiftUI.md index cbc3185..82b7cbb 100644 --- a/AgentGuidelines/Guidelines/Swift/SwiftUI.md +++ b/AgentGuidelines/Guidelines/Swift/SwiftUI.md @@ -60,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..9d00f99 --- /dev/null +++ b/AgentGuidelines/Guidelines/Xcode/ProjectSettings.md @@ -0,0 +1,80 @@ +# Xcode Project Settings + +Use Apple's [Build settings reference](https://developer.apple.com/documentation/xcode/build-settings-reference) for the current setting names and behavior. Apply this baseline to every checked-in Xcode project. + +## Project-level ownership + +Define the required settings in every project build configuration so application, extension, framework, unit-test, UI-test, and other targets inherit one baseline. An `.xcconfig` counts as project-level ownership only when the project's build configurations reference it; a target-only configuration does not. + +Do not satisfy this policy with repeated target-level values. Remove redundant target copies so inheritance remains visible. A target may override a project value only under a documented exception that names that target and setting. + +## Required baseline + +Set all warning policies to `YES`: + +- `GCC_TREAT_WARNINGS_AS_ERRORS` +- `MTL_TREAT_WARNINGS_AS_ERRORS` +- `SWIFT_TREAT_WARNINGS_AS_ERRORS` + +Set the concurrency baseline: + +- `SWIFT_APPROACHABLE_CONCURRENCY = YES` +- `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` +- `SWIFT_STRICT_CONCURRENCY = complete` + +For application targets, set `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO` at project level so every configuration generates Apple's [`ITSAppUsesNonExemptEncryption`](https://developer.apple.com/documentation/bundleresources/information-property-list/itsappusesnonexemptencryption) Boolean with the value `NO`. This declares that the shipped app does not use non-exempt encryption and avoids App Store Connect's recurring missing-compliance questionnaire. Verify the generated Boolean in the built app before upload and reassess it whenever the app or a linked dependency introduces cryptography. + +Set `SWIFT_VERSION` to the newest stable Swift language mode supported by the selected Xcode. The current language mode is Swift 6, serialized as `SWIFT_VERSION = 6.0`; move to 7, 8, 9, and later stable modes when their supporting Xcode releases are adopted. Do not confuse the compiler's minor release, such as Swift 6.4, with the Swift language mode. + +Inspect every build setting exposed by the selected Xcode whose name begins with `SWIFT_UPCOMING_FEATURE_`. Enable each feature that remains opt-in under the selected Swift language mode. Do not set an upcoming-feature flag when that language mode already enables the feature unconditionally: Swift diagnoses some redundant flags, and warnings-as-errors can turn that diagnostic into a build failure. `SWIFT_APPROACHABLE_CONCURRENCY` enables a subset of concurrency features but does not replace the applicable explicit upcoming-feature settings. + +The Xcode 27 inventory to evaluate is: + +- `SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE` +- `SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN` +- `SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION` +- `SWIFT_UPCOMING_FEATURE_DYNAMIC_ACTOR_ISOLATION` +- `SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY` +- `SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES` +- `SWIFT_UPCOMING_FEATURE_GLOBAL_ACTOR_ISOLATED_TYPES_USABILITY` +- `SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY` +- `SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS` +- `SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS` +- `SWIFT_UPCOMING_FEATURE_INFER_ISOLATED_CONFORMANCES` +- `SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES` +- `SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT` +- `SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES` +- `SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY` +- `SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY` +- `SWIFT_UPCOMING_FEATURE_NONISOLATED_NONSENDING_BY_DEFAULT` +- `SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION` + +Treat this list as discovery input for Xcode 27, not as a set of flags that must all be present and not as a permanent exhaustive list. When adopting a newer Xcode, compare its build settings with this prefix and evaluate newly exposed settings against the selected Swift language mode. Require each still-upcoming feature at project level; omit each feature already incorporated into the language mode. + +## Swift package parity + +Whenever this Xcode project baseline adds, removes, or changes a Swift or Clang compiler policy, evaluate whether Swift Package Manager exposes a semantically equivalent setting and whether that policy is appropriate for reusable packages. When it is, update the [Swift package compiler-settings baseline](../Packages.md#compiler-settings-baseline), the Swift-package completion audit, and their structural validation in the same change. Do not mechanically copy application-only policy such as `Info.plist` declarations, Metal settings without a SwiftPM equivalent, or default MainActor isolation into the package baseline. + +## Audit procedure + +1. Identify the selected Xcode version and its newest stable Swift language mode. +2. Inspect the `PBXProject` build configurations and any project-level `.xcconfig` files. Confirm every Debug, Release, and custom configuration defines the complete baseline. +3. Compare the active Xcode's build settings with the `SWIFT_UPCOMING_FEATURE_` prefix so newly introduced settings are not missed. Use the setting documentation and compiler diagnostics for the selected language mode to distinguish still-upcoming features from features that are already unconditional. +4. Enumerate every target, including application targets and unit-test and UI-test targets, and every supported configuration. Use Xcode project-aware tooling or `xcodebuild -showBuildSettings` to verify the effective values. For each application target, also inspect the built `Info.plist` and require the `ITSAppUsesNonExemptEncryption` Boolean to be `NO`. +5. Inspect target build configurations for redundant copies, disabling values, overrides, or upcoming-feature flags that are redundant in the selected language mode. Remove redundant copies and resolve undocumented overrides. +6. Build the relevant configurations and treat every warning as a failure unless an applicable documented exception explicitly covers the setting that would otherwise promote it. + +## Exceptions + +An incompatible project or target requirement may specialize one or more settings only when the nearest applicable `AGENTS.md`, or durable project documentation linked from it, records: + +- each exact setting name; +- the affected project, configurations, and targets; +- the concrete requirement that prevents the baseline value; +- the replacement value or omitted setting and its engineering impact; +- compensating validation where relevant; and +- the condition for removing or revisiting the exception. + +The audit accepts an applicable documented exception and reports the deviation without failing the project for that setting. A transient discussion, generic statement that a project uses different settings, or the existing target configuration by itself is not an exception. + +An application that uses non-exempt encryption must set `INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = YES` and document that exact setting as an exception, including the cryptography that requires the declaration and the resulting export-compliance workflow. The audit accepts a generated `ITSAppUsesNonExemptEncryption = YES` under that documented exception instead of reporting a false positive. diff --git a/AgentGuidelines/README.md b/AgentGuidelines/README.md index b6ab81a..cec2d20 100644 --- a/AgentGuidelines/README.md +++ b/AgentGuidelines/README.md @@ -1,10 +1,11 @@

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

# Agent Guidelines @@ -60,12 +61,14 @@ The subtree does not automatically import every guide into an agent's context. A ## Guideline catalog - [Agent workflow and tool execution](Guidelines/AgentWorkflow.md) +- [App Store metadata](Guidelines/AppStore.md) - [CI/CD](Guidelines/CICD.md) - [Development and reusability](Guidelines/Development.md) - [Documentation](Guidelines/Documentation.md) +- [Git ignore files](Guidelines/Git/IgnoreFiles.md) - [Git repositories and SSH-first cloning](Guidelines/Git/Repositories.md) - [GitHub pull requests](Guidelines/GitHub/PullRequests.md) -- [Localization](Guidelines/Swift/Localization.md) +- [Localization](Guidelines/Localization.md) - [Logging](Guidelines/Logging.md) - [Redux architecture and physical folder organization](Guidelines/Architecture/Redux.md) - [Swift](Guidelines/Swift/Swift.md) @@ -75,6 +78,7 @@ The subtree does not automatically import every guide into an agent's context. A - [SwiftUI](Guidelines/Swift/SwiftUI.md) - [Unit and integration testing](Guidelines/Testing/UnitTesting.md) - [Xcode MCP and visual verification](Guidelines/Xcode/MCP.md) +- [Xcode project settings](Guidelines/Xcode/ProjectSettings.md) - [Xcode security audits](Guidelines/Xcode/Security.md) Only reference the guides that apply. Agent workflow normally applies to both applications and packages. A UI-agnostic package normally also uses Swift, style, testing, documentation, logging, packages, CI/CD, and Xcode guidance, but not Redux or SwiftUI guidance. @@ -87,8 +91,8 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.18 \ - --squash + 0.0.33 \ + --squash ``` Swift consumers that adopt the shared formatter expose its configuration at the repository root so Xcode and other tools discover it: @@ -105,7 +109,9 @@ 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. Keep the version-marked code-review contract directly in the repository-root `AGENTS.md`; Markdown links to shared guides are navigation, not automatic instruction includes. +Copy and adapt [the consumer template](Templates/AGENTS.md). Keep the consumer file small: describe the product or package, map its concrete physical folders, point to the applicable shared guides, and state only genuine exceptions. Keep the version-marked code-review contract, documentation-maintenance contract, external-dependency contract, and runtime-observability contract directly in the repository-root `AGENTS.md`; Markdown links to shared guides are navigation, not automatic instruction includes. + +Copy the shared [`.gitignore` template](Templates/.gitignore) into a new Xcode project or Swift package. Keep authored project files, workspaces, and package lockfiles eligible for version control, and follow the [Git ignore guidance](Guidelines/Git/IgnoreFiles.md) when an established consumer needs an additional project-specific rule. ### Configure global Codex instructions @@ -128,10 +134,10 @@ ln -s ../../AgentGuidelines/.agents/skills/agent-guidelines-audit \ Validate the checked-in consumer integration directly or through the completion-audit skill: ```sh -python3 AgentGuidelines/Scripts/validate_consumer_setup.py +AgentGuidelines/Scripts/validate_consumer_setup.swift ``` -The validator checks the version-marked root Code Review contract, 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. +The native Swift validator checks the version-marked root Code Review, Documentation Maintenance, External Dependency, and Runtime Observability contracts, Codex subtree-review scope, `.gitattributes`, local guide links, and the audit-skill symlink. When the root `AGENTS.md` links the shared Swift-format guide, it also requires both configuration symlinks and a non-mutating `lint-strict` CI invocation. Pass `--require-swift-format` only when auditing formatter adoption before adding that guide link. ## Update a consumer @@ -141,11 +147,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.18 \ - --squash + 0.0.33 \ + --squash ``` -Confirm `AgentGuidelines/VERSION`, review the subtree diff, synchronize the marked code-review contract when its version changes, run `python3 AgentGuidelines/Scripts/validate_consumer_setup.py`, 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, external-dependency contract, and runtime-observability contract when their versions change, run `AgentGuidelines/Scripts/validate_consumer_setup.swift`, and run the consumer's relevant tests. Keep the subtree update in its own commit, and identify the old and new versions plus the central release or pull request in the consumer pull-request description. Updates are intentionally not automatic: one guideline release cannot silently change every project. ## Maintain the source of truth @@ -158,7 +164,7 @@ Confirm `AgentGuidelines/VERSION`, review the subtree diff, synchronize the mark 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/validate_consumer_setup.py b/AgentGuidelines/Scripts/validate_consumer_setup.py deleted file mode 100755 index 687e1a1..0000000 --- a/AgentGuidelines/Scripts/validate_consumer_setup.py +++ /dev/null @@ -1,304 +0,0 @@ -#!/usr/bin/env python3 -"""Validate a consumer repository's checked-in agent-guidelines integration.""" - -from __future__ import annotations - -import argparse -import re -import sys -from pathlib import Path - - -GUIDELINES_ROOT = Path(__file__).resolve().parents[1] -CONTRACT_BEGIN = "" -CONTRACT_END = "" -MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") -SWIFT_FORMAT_GUIDE = "AgentGuidelines/Guidelines/Swift/SwiftFormat.md" -STRICT_FORMAT_COMMAND = re.compile( - r"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?" - r"AgentGuidelines/Scripts/swift_format\.sh\s+lint-strict(?=\s|\\|$)", -) -MUTATING_FORMAT_COMMAND = re.compile( - r"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?" - r"AgentGuidelines/Scripts/swift_format\.sh\s+format(?:-and-lint)?(?=\s|\\|$)", -) -GENERATED_ATTRIBUTE = re.compile( - r"^\s*AgentGuidelines/\*\*\s+linguist-generated\s*$", - re.MULTILINE, -) - - -def read_text(path: Path, errors: list[str], label: str) -> str | None: - try: - return path.read_text(encoding="utf-8") - except OSError as error: - errors.append(f"{label}: cannot read {path}: {error}") - return None - - -def extract_contract(contents: str, errors: list[str], label: str) -> str | None: - if contents.count(CONTRACT_BEGIN) != 1 or contents.count(CONTRACT_END) != 1: - errors.append( - f"{label}: expected exactly one {CONTRACT_BEGIN!r} and {CONTRACT_END!r}" - ) - return None - - start = contents.index(CONTRACT_BEGIN) - end = contents.index(CONTRACT_END, start) + len(CONTRACT_END) - return contents[start:end].strip() - - -def validate_symlink(path: Path, expected: Path, errors: list[str], label: str) -> None: - if not path.is_symlink(): - errors.append(f"{label}: {path} must be a symlink to {expected}") - return - - if not path.exists(): - errors.append(f"{label}: {path} is a broken symlink") - return - - if path.resolve() != expected.resolve(): - errors.append(f"{label}: {path} resolves to {path.resolve()}, expected {expected.resolve()}") - - -def validate_agent_links( - agents_path: Path, - contents: str, - errors: list[str], -) -> None: - for raw_target in MARKDOWN_LINK.findall(contents): - target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] - if not target or target.startswith(("#", "http://", "https://", "mailto:")): - continue - - resolved = (agents_path.parent / target).resolve() - if not resolved.exists(): - errors.append(f"AGENTS.md: missing local link target {raw_target!r}") - - -def adopts_swift_format(contents: str) -> bool: - for raw_target in MARKDOWN_LINK.findall(contents): - target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] - if target.endswith(SWIFT_FORMAT_GUIDE): - return True - return False - - -def shell_invocations(contents: str, command: re.Pattern[str]) -> list[str]: - lines = contents.splitlines() - invocations: list[str] = [] - index = 0 - while index < len(lines): - line = lines[index] - if line.lstrip().startswith("#") or not command.search(line): - index += 1 - continue - - invocation = [line] - while invocation[-1].rstrip().endswith("\\") and index + 1 < len(lines): - index += 1 - invocation.append(lines[index]) - invocations.append("\n".join(invocation)) - index += 1 - return invocations - - -def validate_swift_format_ci(consumer_root: Path, errors: list[str]) -> None: - workflows_root = consumer_root / ".github" / "workflows" - workflows = sorted(workflows_root.glob("*.yml")) + sorted( - workflows_root.glob("*.yaml") - ) - if not workflows: - errors.append( - "consumer Swift format CI: no GitHub Actions workflows found under " - ".github/workflows" - ) - return - - strict_workflows: list[tuple[Path, str, list[str]]] = [] - for workflow in workflows: - contents = read_text( - workflow, - errors, - f"consumer Swift format CI workflow {workflow.relative_to(consumer_root)}", - ) - if contents is None: - continue - if shell_invocations(contents, MUTATING_FORMAT_COMMAND): - errors.append( - "consumer Swift format CI: " - f"{workflow.relative_to(consumer_root)} must not mutate sources with " - "format or format-and-lint" - ) - invocations = shell_invocations(contents, STRICT_FORMAT_COMMAND) - if invocations: - strict_workflows.append((workflow, contents, invocations)) - - if not strict_workflows: - errors.append( - "consumer Swift format CI: missing " - "'AgentGuidelines/Scripts/swift_format.sh lint-strict' invocation" - ) - return - - if not any( - re.search(r"(?m)^\s*pull_request\s*:", contents) - for _, contents, _ in strict_workflows - ): - errors.append("consumer Swift format CI: lint-strict does not run for pull requests") - - if not any( - re.search(r"(?m)^\s*push\s*:", contents) - and re.search(r"(?m)^\s*-?\s*main\s*$|branches\s*:\s*\[[^]]*\bmain\b", contents) - for _, contents, _ in strict_workflows - ): - errors.append("consumer Swift format CI: lint-strict does not run for pushes to main") - - if (consumer_root / "Package.swift").is_file(): - required_paths = ["Package.swift"] - required_paths.extend( - name for name in ("Sources", "Tests") if (consumer_root / name).is_dir() - ) - combined = "\n".join( - invocation - for _, _, invocations in strict_workflows - for invocation in invocations - ) - for path in required_paths: - if not re.search(rf"(? None: - consumer_root = consumer_root.resolve() - guidelines_root = guidelines_root.resolve() - subtree = consumer_root / "AgentGuidelines" - - if not subtree.exists(): - errors.append(f"consumer root: missing {subtree}") - elif subtree.resolve() != guidelines_root: - errors.append( - f"consumer root: {subtree} resolves to {subtree.resolve()}, " - f"expected the active guidelines at {guidelines_root}" - ) - - version_path = guidelines_root / "VERSION" - version = read_text(version_path, errors, "AgentGuidelines/VERSION") - if version is not None and not version.strip(): - errors.append("AgentGuidelines/VERSION: version is empty") - - template_path = guidelines_root / "Templates" / "AGENTS.md" - agents_path = consumer_root / "AGENTS.md" - template = read_text(template_path, errors, "AgentGuidelines template") - agents = read_text(agents_path, errors, "consumer AGENTS.md") - - swift_format_adopted = require_swift_format - if template is not None and agents is not None: - expected_contract = extract_contract(template, errors, "AgentGuidelines template") - actual_contract = extract_contract(agents, errors, "consumer AGENTS.md") - if ( - expected_contract is not None - and actual_contract is not None - and actual_contract != expected_contract - ): - errors.append( - "consumer AGENTS.md: code-review contract does not match " - "AgentGuidelines/Templates/AGENTS.md" - ) - - required_scope_values = ( - "## Codex review scope", - "AgentGuidelines/**", - "exact tagged-tree provenance", - ".gitattributes", - ) - for value in required_scope_values: - if value not in agents: - errors.append(f"consumer AGENTS.md: missing Codex review scope value {value!r}") - - validate_agent_links(agents_path, agents, errors) - swift_format_adopted = swift_format_adopted or adopts_swift_format(agents) - - attributes_path = consumer_root / ".gitattributes" - attributes = read_text(attributes_path, errors, "consumer .gitattributes") - if attributes is not None and not GENERATED_ATTRIBUTE.search(attributes): - errors.append( - "consumer .gitattributes: missing exact " - "'AgentGuidelines/** linguist-generated' rule" - ) - - expected_skill = guidelines_root / ".agents" / "skills" / "agent-guidelines-audit" - consumer_skill = consumer_root / ".agents" / "skills" / "agent-guidelines-audit" - validate_symlink(consumer_skill, expected_skill, errors, "consumer audit skill") - - formatter_links = { - ".swift-format": guidelines_root / "Configurations" / "Swift" / ".swift-format", - ".editorconfig": guidelines_root / "Configurations" / "Swift" / ".editorconfig", - } - for name, expected in formatter_links.items(): - path = consumer_root / name - if swift_format_adopted or path.is_symlink(): - validate_symlink(path, expected, errors, f"consumer {name}") - - if swift_format_adopted: - validate_swift_format_ci(consumer_root, errors) - - -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--consumer-root", - type=Path, - help="Consumer repository root. Defaults to the parent of an AgentGuidelines subtree.", - ) - parser.add_argument( - "--require-swift-format", - action="store_true", - help=( - "Require shared Swift-format configuration links and strict CI even when " - "AGENTS.md does not link the Swift-format guide." - ), - ) - return parser.parse_args() - - -def main() -> int: - arguments = parse_arguments() - consumer_root = arguments.consumer_root - if consumer_root is None: - if GUIDELINES_ROOT.name != "AgentGuidelines": - print( - "Consumer setup validation failed:\n" - "- --consumer-root is required when this checkout is not installed " - "as an AgentGuidelines subtree" - ) - return 1 - consumer_root = GUIDELINES_ROOT.parent - - errors: list[str] = [] - validate_consumer_setup( - errors, - consumer_root, - require_swift_format=arguments.require_swift_format, - ) - if errors: - print("Consumer setup validation failed:") - for error in errors: - print(f"- {error}") - return 1 - - version = (GUIDELINES_ROOT / "VERSION").read_text(encoding="utf-8").strip() - print(f"Validated consumer setup for agent-guidelines {version}.") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/AgentGuidelines/Scripts/validate_consumer_setup.swift b/AgentGuidelines/Scripts/validate_consumer_setup.swift new file mode 100755 index 0000000..e5b5c37 --- /dev/null +++ b/AgentGuidelines/Scripts/validate_consumer_setup.swift @@ -0,0 +1,465 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +let guidelinesRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() +let contractBegin = "" +let contractEnd = "" +let documentationContractBegin = "" +let documentationContractEnd = "" +let externalDependencyContractBegin = "" +let externalDependencyContractEnd = "" +let observabilityContractBegin = "" +let observabilityContractEnd = "" +let markdownLinkPattern = #"\[[^\]]+\]\(([^)]+)\)"# +let swiftFormatGuide = "AgentGuidelines/Guidelines/Swift/SwiftFormat.md" +let strictFormatCommandPattern = + #"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?AgentGuidelines/Scripts/swift_format\.sh\s+lint-strict(?=\s|\\|$)"# +let mutatingFormatCommandPattern = + #"(?m)^[ \t]*(?:-\s+)?(?:run:\s*)?(?:\./)?AgentGuidelines/Scripts/swift_format\.sh\s+format(?:-and-lint)?(?=\s|\\|$)"# +let generatedAttributePattern = #"(?m)^\s*AgentGuidelines/\*\*\s+linguist-generated\s*$"# +let readmeLicenseHeadingPattern = #"(?im)^[ \t]{0,3}#{1,6}[ \t]+license[ \t]*#*[ \t]*$"# +let readmeSetextUnderlinePattern = #"^[ \t]{0,3}(?:=+|-+)[ \t]*$"# +let readmeLicenseParagraphPattern = + #"(?i)^.+?\s+is\s+available\s+under\s+the\s+.+?\s+license\.\s+See\s+\[LICENSE\]\((?:\./)?LICENSE\)\.$"# + +/// Parsed consumer-validation command-line values. +struct Arguments { + var consumerRoot: String? + var requireSwiftFormat = false +} + +/// Returns all regular-expression matches in a string. +func matches(_ pattern: String, in value: String) -> [NSTextCheckingResult] { + guard let expression = try? NSRegularExpression(pattern: pattern) else { + return [] + } + return expression.matches(in: value, range: NSRange(value.startIndex.. String? { + let range = match.range(at: index) + guard range.location != NSNotFound, let swiftRange = Range(range, in: value) else { + return nil + } + return String(value[swiftRange]) +} + +/// Reads UTF-8 text and records a labeled error on failure. +func readText(_ url: URL, errors: inout [String], label: String) -> String? { + do { + return try String(contentsOf: url, encoding: .utf8) + } catch { + errors.append("\(label): cannot read \(url.path): \(error.localizedDescription)") + return nil + } +} + +/// Extracts one uniquely marked contract block. +func extractMarkedBlock( + _ contents: String, + begin: String, + end: String, + errors: inout [String], + label: String +) -> String? { + guard contents.components(separatedBy: begin).count - 1 == 1, + contents.components(separatedBy: end).count - 1 == 1, + let start = contents.range(of: begin), + let finish = contents.range(of: end, range: start.upperBound..")) + target = target.components(separatedBy: "#").first ?? target + if target.isEmpty || ["#", "http://", "https://", "mailto:"].contains(where: target.hasPrefix) { + continue + } + let resolved = agentsURL.deletingLastPathComponent().appendingPathComponent(target).standardizedFileURL + if !FileManager.default.fileExists(atPath: resolved.path) { + errors.append("AGENTS.md: missing local link target '\(target)'") + } + } +} + +/// Returns whether root instructions link the shared Swift-format guide. +func adoptsSwiftFormat(_ contents: String) -> Bool { + for match in matches(markdownLinkPattern, in: contents) { + guard var target = capture(1, from: match, in: contents) else { + continue + } + target = target.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters( + in: CharacterSet(charactersIn: "<>")) + target = target.components(separatedBy: "#").first ?? target + if target.hasSuffix(swiftFormatGuide) { + return true + } + } + return false +} + +/// Returns complete shell invocations matching a command pattern. +func shellInvocations(_ contents: String, pattern: String) -> [String] { + let lines = contents.components(separatedBy: .newlines) + var invocations: [String] = [] + var index = 0 + while index < lines.count { + let line = lines[index] + if line.trimmingCharacters(in: .whitespaces).hasPrefix("#") || matches(pattern, in: line).isEmpty { + index += 1 + continue + } + var invocation = [line] + while invocation.last?.trimmingCharacters(in: .whitespaces).hasSuffix("\\") == true, + index + 1 < lines.count + { + index += 1 + invocation.append(lines[index]) + } + invocations.append(invocation.joined(separator: "\n")) + index += 1 + } + return invocations +} + +/// Returns direct files with one of the requested extensions. +func files(in directory: URL, extensions: Set) -> [URL] { + guard + let values = try? FileManager.default.contentsOfDirectory( + at: directory, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles] + ) + else { + return [] + } + return values.filter { extensions.contains($0.pathExtension.lowercased()) }.sorted { $0.path < $1.path } +} + +/// Returns a Markdown fence marker and run length when a line can open or close a fenced code block. +func markdownFence(in line: String) -> (character: Character, length: Int, suffix: Substring)? { + let indentation = line.prefix(while: { $0 == " " }).count + let content = line.dropFirst(indentation) + guard indentation <= 3, let character = content.first, character == "`" || character == "~" + else { + return nil + } + let length = content.prefix(while: { $0 == character }).count + guard length >= 3 else { return nil } + return (character, length, content.dropFirst(length)) +} + +/// Returns whether a line is eligible to be the title of a Setext License heading. +func isSetextLicenseTitle(_ line: String) -> Bool { + let indentation = line.prefix(while: { $0 == " " }).count + guard indentation <= 3 else { return false } + return line.dropFirst(indentation).trimmingCharacters(in: .whitespaces) + .localizedCaseInsensitiveCompare("License") == .orderedSame +} + +/// Validates package README license content without inspecting vendored or non-package documentation. +func validatePackageReadme(consumerRoot: URL, errors: inout [String]) { + guard FileManager.default.fileExists(atPath: consumerRoot.appendingPathComponent("Package.swift").path) else { + return + } + let readmeURL = consumerRoot.appendingPathComponent("README.md") + guard FileManager.default.fileExists(atPath: readmeURL.path), + let contents = readText(readmeURL, errors: &errors, label: "consumer package README") + else { + return + } + var outsideFences: [String] = [] + var activeFence: (character: Character, length: Int)? + for line in contents.components(separatedBy: .newlines) { + if let currentFence = activeFence { + if let marker = markdownFence(in: line), marker.character == currentFence.character, + marker.length >= currentFence.length, + marker.suffix.trimmingCharacters(in: .whitespaces).isEmpty + { + activeFence = nil + } + outsideFences.append("") + } else if let marker = markdownFence(in: line), + marker.character == "~" || !marker.suffix.contains("`") + { + activeFence = (marker.character, marker.length) + outsideFences.append("") + } else { + outsideFences.append(line) + } + } + let governedContents = outsideFences.joined(separator: "\n") + let lines = governedContents.components(separatedBy: .newlines) + let hasSetextLicenseHeading = lines.indices.dropLast().contains { index in + isSetextLicenseTitle(lines[index]) && !matches(readmeSetextUnderlinePattern, in: lines[index + 1]).isEmpty + } + if !matches(readmeLicenseHeadingPattern, in: governedContents).isEmpty || hasSetextLicenseHeading { + errors.append("consumer package README: README.md must not contain a dedicated License heading") + } + let paragraphs = governedContents.components(separatedBy: "\n\n").map { + $0.split(whereSeparator: \.isNewline).joined(separator: " ") + .trimmingCharacters(in: .whitespacesAndNewlines) + } + if paragraphs.contains(where: { !matches(readmeLicenseParagraphPattern, in: $0).isEmpty }) { + errors.append("consumer package README: README.md must not contain a standalone license-description paragraph") + } +} + +/// Validates non-mutating Swift-format CI integration. +func validateSwiftFormatCI(consumerRoot: URL, errors: inout [String]) { + let workflowsRoot = consumerRoot.appendingPathComponent(".github/workflows") + let workflows = files(in: workflowsRoot, extensions: ["yml", "yaml"]) + if workflows.isEmpty { + errors.append("consumer Swift format CI: no GitHub Actions workflows found under .github/workflows") + return + } + var strictWorkflows: [(URL, String, [String])] = [] + for workflow in workflows { + let relative = workflow.path.replacingOccurrences(of: consumerRoot.path + "/", with: "") + guard let contents = readText(workflow, errors: &errors, label: "consumer Swift format CI workflow \(relative)") + else { + continue + } + if !shellInvocations(contents, pattern: mutatingFormatCommandPattern).isEmpty { + errors.append( + "consumer Swift format CI: \(relative) must not mutate sources with format or format-and-lint") + } + let invocations = shellInvocations(contents, pattern: strictFormatCommandPattern) + if !invocations.isEmpty { + strictWorkflows.append((workflow, contents, invocations)) + } + } + if strictWorkflows.isEmpty { + errors.append( + "consumer Swift format CI: missing 'AgentGuidelines/Scripts/swift_format.sh lint-strict' invocation") + return + } + if !strictWorkflows.contains(where: { !matches(#"(?m)^\s*pull_request\s*:"#, in: $0.1).isEmpty }) { + errors.append("consumer Swift format CI: lint-strict does not run for pull requests") + } + let pushPattern = #"(?m)^\s*push\s*:"# + let mainPattern = #"(?m)^\s*-?\s*main\s*$|branches\s*:\s*\[[^]]*\bmain\b"# + if !strictWorkflows.contains(where: { + !matches(pushPattern, in: $0.1).isEmpty && !matches(mainPattern, in: $0.1).isEmpty + }) { + errors.append("consumer Swift format CI: lint-strict does not run for pushes to main") + } + if FileManager.default.fileExists(atPath: consumerRoot.appendingPathComponent("Package.swift").path) { + var requiredPaths = ["Package.swift"] + for name in ["Sources", "Tests"] { + var isDirectory: ObjCBool = false + if FileManager.default.fileExists( + atPath: consumerRoot.appendingPathComponent(name).path, + isDirectory: &isDirectory + ), isDirectory.boolValue { + requiredPaths.append(name) + } + } + let combined = strictWorkflows.flatMap(\.2).joined(separator: "\n") + for path in requiredPaths { + let escaped = NSRegularExpression.escapedPattern(for: path) + if matches("(? Arguments { + var arguments = Arguments() + var index = 0 + while index < values.count { + let value = values[index] + switch value { + case "--help": + print("Usage: validate_consumer_setup.swift [--consumer-root ] [--require-swift-format]") + exit(0) + case "--require-swift-format": + arguments.requireSwiftFormat = true + index += 1 + case "--consumer-root": + guard index + 1 < values.count else { + throw NSError( + domain: "ConsumerSetup", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "missing value for --consumer-root" + ]) + } + arguments.consumerRoot = values[index + 1] + index += 2 + default: + throw NSError( + domain: "ConsumerSetup", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "unknown argument: \(value)" + ]) + } + } + return arguments +} + +/// Writes text to standard error. +func writeError(_ value: String) { + FileHandle.standardError.write(Data((value + "\n").utf8)) +} + +/// Runs consumer integration validation. +func main() -> Int32 { + do { + let arguments = try parseArguments(Array(CommandLine.arguments.dropFirst())) + let root: URL + if let consumerRoot = arguments.consumerRoot { + root = URL(fileURLWithPath: consumerRoot) + } else { + guard guidelinesRoot.lastPathComponent == "AgentGuidelines" else { + print( + "Consumer setup validation failed:\n" + + "- --consumer-root is required when this checkout is not installed as an AgentGuidelines subtree" + ) + return 1 + } + root = guidelinesRoot.deletingLastPathComponent() + } + var errors: [String] = [] + validateConsumerSetup( + errors: &errors, + consumerRoot: root, + requireSwiftFormat: arguments.requireSwiftFormat + ) + if !errors.isEmpty { + print("Consumer setup validation failed:") + for error in errors { print("- \(error)") } + return 1 + } + let version = try String( + contentsOf: guidelinesRoot.appendingPathComponent("VERSION"), + encoding: .utf8 + ).trimmingCharacters(in: .whitespacesAndNewlines) + print("Validated consumer setup for agent-guidelines \(version).") + return 0 + } catch { + writeError("Consumer setup validation failed: \(error.localizedDescription)") + return 2 + } +} + +exit(main()) diff --git a/AgentGuidelines/Scripts/validate_guidelines.py b/AgentGuidelines/Scripts/validate_guidelines.py deleted file mode 100644 index 95fa1e3..0000000 --- a/AgentGuidelines/Scripts/validate_guidelines.py +++ /dev/null @@ -1,349 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the structure and public safety of the guideline repository.""" - -from __future__ import annotations - -import json -import os -import re -import sys -from pathlib import Path, PurePosixPath - - -ROOT = Path(__file__).resolve().parents[1] -README = ROOT / "README.md" -VERSION = ROOT / "VERSION" -CHANGELOG = ROOT / "CHANGELOG.md" -SWIFT_FORMAT_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".swift-format" -EDITOR_CONFIGURATION = ROOT / "Configurations" / "Swift" / ".editorconfig" -SWIFT_FORMAT_SCRIPT = ROOT / "Scripts" / "swift_format.sh" -SWIFT_FORMAT_GUIDELINE = ROOT / "Guidelines" / "Swift" / "SwiftFormat.md" -CONSUMER_SETUP_SCRIPT = ROOT / "Scripts" / "validate_consumer_setup.py" -AUDIT_SKILL = ROOT / ".agents" / "skills" / "agent-guidelines-audit" / "SKILL.md" -DEVELOPMENT_GUIDELINE = ROOT / "Guidelines" / "Development.md" -AGENTS_TEMPLATE = ROOT / "Templates" / "AGENTS.md" -EXPECTED_SWIFT_FORMAT_RULES = { - "AllPublicDeclarationsHaveDocumentation": False, - "AlwaysUseLiteralForEmptyCollectionInit": True, - "AlwaysUseLowerCamelCase": True, - "AmbiguousTrailingClosureOverload": True, - "AvoidRetroactiveConformances": True, - "BeginDocumentationCommentWithOneLineSummary": False, - "DoNotUseSemicolons": True, - "DontRepeatTypeInStaticProperties": True, - "FileScopedDeclarationPrivacy": True, - "FullyIndirectEnum": True, - "GroupNumericLiterals": True, - "IdentifiersMustBeASCII": True, - "NeverForceUnwrap": False, - "NeverUseForceTry": True, - "NeverUseImplicitlyUnwrappedOptionals": False, - "NoAccessLevelOnExtensionDeclaration": True, - "NoAssignmentInExpressions": True, - "NoBlockComments": True, - "NoCasesWithOnlyFallthrough": True, - "NoEmptyLinesOpeningClosingBraces": True, - "NoEmptyTrailingClosureParentheses": True, - "NoLabelsInCasePatterns": True, - "NoLeadingUnderscores": False, - "NoParensAroundConditions": True, - "NoPlaygroundLiterals": True, - "NoVoidReturnOnFunctionSignature": True, - "OmitExplicitReturns": False, - "OneCasePerLine": True, - "OneVariableDeclarationPerLine": True, - "OnlyOneTrailingClosureArgument": True, - "OrderedImports": True, - "ReplaceForEachWithForLoop": True, - "ReturnVoidInsteadOfEmptyTuple": True, - "TypeNamesShouldBeCapitalized": True, - "UseEarlyExits": False, - "UseExplicitNilCheckInConditions": True, - "UseLetInEveryBoundCaseVariable": True, - "UseShorthandTypeNames": True, - "UseSingleLinePropertyGetter": True, - "UseSynthesizedInitializer": True, - "UseTripleSlashForDocumentationComments": True, - "UseWhereClausesInForLoops": True, - "ValidateDocumentationComments": True, -} - -MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") -SEMVER = re.compile( - r"^(0|[1-9][0-9]*)\." - r"(0|[1-9][0-9]*)\." - r"(0|[1-9][0-9]*)" - r"(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" - r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?" - r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$" -) -FORBIDDEN = { - "/" + "Users" + "/": "personal absolute path", - "file" + "://": "local file URL", - "mobile-ios-" + "chauffeur": "work-repository identifier", - "black" + "lane": "work-repository identifier", -} - - -def text_files() -> list[Path]: - suffixes = {".md", ".py", ".swift", ".yml", ".yaml", ".txt"} - files = [path for path in ROOT.rglob("*") if path.is_file() and path.suffix in suffixes] - files.extend(path for path in (ROOT / "VERSION", ROOT / "LICENSE") if path.is_file()) - files.extend( - path - for path in (SWIFT_FORMAT_CONFIGURATION, EDITOR_CONFIGURATION) - if path.is_file() - ) - return sorted(set(files)) - - -def resolve_link(source: Path, raw_target: str) -> Path | None: - target = raw_target.strip().strip("<>").split("#", maxsplit=1)[0] - if not target or target.startswith(("#", "http://", "https://", "mailto:")): - return None - - parts = PurePosixPath(target).parts - if "AgentGuidelines" in parts: - index = parts.index("AgentGuidelines") - return ROOT.joinpath(*parts[index + 1 :]).resolve() - - return (source.parent / target).resolve() - - -def validate_links(errors: list[str]) -> None: - for source in sorted(ROOT.rglob("*.md")): - for raw_target in MARKDOWN_LINK.findall(source.read_text(encoding="utf-8")): - resolved = resolve_link(source, raw_target) - if resolved is not None and not resolved.exists(): - relative_source = source.relative_to(ROOT) - errors.append(f"{relative_source}: missing link target {raw_target!r}") - - -def validate_catalog(errors: list[str]) -> None: - readme = README.read_text(encoding="utf-8") - for guide in sorted((ROOT / "Guidelines").rglob("*.md")): - relative = guide.relative_to(ROOT).as_posix() - if f"]({relative})" not in readme: - errors.append(f"README.md: guideline is not cataloged: {relative}") - - -def validate_version(errors: list[str]) -> None: - version = VERSION.read_text(encoding="utf-8").strip() - if not SEMVER.fullmatch(version): - errors.append(f"VERSION: invalid semantic version {version!r}") - - changelog = CHANGELOG.read_text(encoding="utf-8") - if f"## [{version}]" not in changelog: - errors.append(f"CHANGELOG.md: missing release heading for {version}") - - -def validate_readme_contract(errors: list[str]) -> None: - readme = README.read_text(encoding="utf-8") - required = { - 'alt="Xcode"': "Xcode badge alt text", - "thatfactory/agent-guidelines/actions/workflows/ci.yml": "CI badge repository", - "--prefix=AgentGuidelines": "subtree destination", - "https://github.com/thatfactory/agent-guidelines.git": "subtree remote", - "git subtree add": "subtree installation command", - "git subtree pull": "subtree update command", - "AgentGuidelines/** linguist-generated": "generated subtree attribute", - "AgentGuidelines/Configurations/Swift/.swift-format": "swift-format symlink command", - "AgentGuidelines/Configurations/Swift/.editorconfig": "EditorConfig symlink command", - ".agents/skills/agent-guidelines-audit": "completion-audit skill setup", - "validate_consumer_setup.py": "consumer setup validation command", - "--require-swift-format": "explicit Swift-format adoption validation", - } - for value, description in required.items(): - if value not in readme: - errors.append(f"README.md: missing {description}: {value!r}") - - -def validate_public_content(errors: list[str]) -> None: - for path in text_files(): - contents = path.read_text(encoding="utf-8") - relative = path.relative_to(ROOT) - for forbidden, description in FORBIDDEN.items(): - if forbidden.lower() in contents.lower(): - errors.append(f"{relative}: contains {description}: {forbidden!r}") - - -def validate_swift_format_configuration(errors: list[str]) -> None: - try: - configuration = json.loads(SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as error: - errors.append(f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: invalid JSON: {error}") - return - - expected_values = { - "indentation": {"spaces": 4}, - "indentSwitchCaseLabels": False, - "lineLength": 120, - "tabWidth": 4, - "version": 1, - } - for key, expected in expected_values.items(): - actual = configuration.get(key) - if actual != expected: - errors.append( - f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " - f"{key} must be {expected!r}, found {actual!r}" - ) - - include_conditional_imports = configuration.get("orderedImports", {}).get( - "includeConditionalImports" - ) - if include_conditional_imports is not True: - errors.append( - f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " - "orderedImports.includeConditionalImports must be True, " - f"found {include_conditional_imports!r}" - ) - - rules = configuration.get("rules") - if not isinstance(rules, dict) or not rules: - errors.append( - f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " - "rules must be an exhaustive non-empty object" - ) - else: - missing = sorted(set(EXPECTED_SWIFT_FORMAT_RULES) - set(rules)) - unexpected = sorted(set(rules) - set(EXPECTED_SWIFT_FORMAT_RULES)) - if missing or unexpected: - errors.append( - f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " - f"rule map mismatch; missing={missing!r}, unexpected={unexpected!r}" - ) - for rule in sorted(set(rules) & set(EXPECTED_SWIFT_FORMAT_RULES)): - expected = EXPECTED_SWIFT_FORMAT_RULES[rule] - actual = rules[rule] - if actual != expected: - errors.append( - f"{SWIFT_FORMAT_CONFIGURATION.relative_to(ROOT)}: " - f"{rule} must be {expected!r}, found {actual!r}" - ) - - -def validate_editor_configuration(errors: list[str]) -> None: - try: - contents = EDITOR_CONFIGURATION.read_text(encoding="utf-8") - except OSError as error: - errors.append( - f"{EDITOR_CONFIGURATION.relative_to(ROOT)}: cannot read configuration: {error}" - ) - return - required = { - "root = true", - "[*.swift]", - "indent_style = space", - "indent_size = 4", - "tab_width = 4", - "max_line_length = 120", - "end_of_line = lf", - "insert_final_newline = true", - "trim_trailing_whitespace = true", - } - for value in sorted(required): - if value not in contents: - errors.append( - f"{EDITOR_CONFIGURATION.relative_to(ROOT)}: missing {value!r}" - ) - - -def validate_swift_format_script(errors: list[str]) -> None: - if not SWIFT_FORMAT_SCRIPT.is_file(): - errors.append(f"{SWIFT_FORMAT_SCRIPT.relative_to(ROOT)}: missing script") - elif not os.access(SWIFT_FORMAT_SCRIPT, os.X_OK): - errors.append(f"{SWIFT_FORMAT_SCRIPT.relative_to(ROOT)}: script is not executable") - - -def validate_swift_format_guideline(errors: list[str]) -> None: - contents = SWIFT_FORMAT_GUIDELINE.read_text(encoding="utf-8") - 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.items(): - if value not in contents: - errors.append( - f"{SWIFT_FORMAT_GUIDELINE.relative_to(ROOT)}: " - f"missing {description}: {value!r}" - ) - - -def validate_consumer_setup_script(errors: list[str]) -> None: - if not CONSUMER_SETUP_SCRIPT.is_file(): - errors.append(f"{CONSUMER_SETUP_SCRIPT.relative_to(ROOT)}: missing script") - elif not os.access(CONSUMER_SETUP_SCRIPT, os.X_OK): - errors.append( - f"{CONSUMER_SETUP_SCRIPT.relative_to(ROOT)}: script is not executable" - ) - - -def validate_audit_skill(errors: list[str]) -> None: - if not AUDIT_SKILL.is_file(): - errors.append(f"{AUDIT_SKILL.relative_to(ROOT)}: missing audit skill") - return - - skill = AUDIT_SKILL.read_text(encoding="utf-8") - required_skill_values = { - "name: agent-guidelines-audit": "skill name", - "before claiming completion": "completion trigger", - "git diff --check": "diff validation", - "validate_consumer_setup.py": "consumer integration validation", - "format-and-lint": "local Swift-format audit", - "lint-strict": "strict Swift-format CI audit", - "no unresolved P0/P1 blocker remains": "Codex review stopping rule", - } - for value, description in required_skill_values.items(): - if value not in skill: - errors.append( - f"{AUDIT_SKILL.relative_to(ROOT)}: missing {description}: {value!r}" - ) - - development = DEVELOPMENT_GUIDELINE.read_text(encoding="utf-8") - if "$agent-guidelines-audit" not in development: - errors.append( - f"{DEVELOPMENT_GUIDELINE.relative_to(ROOT)}: " - "missing mandatory $agent-guidelines-audit invocation" - ) - - agents_template = AGENTS_TEMPLATE.read_text(encoding="utf-8") - if "AgentGuidelines/Guidelines/Development.md" not in agents_template: - errors.append( - f"{AGENTS_TEMPLATE.relative_to(ROOT)}: missing Development.md pointer" - ) - if "## Stack" not in agents_template: - errors.append(f"{AGENTS_TEMPLATE.relative_to(ROOT)}: missing Stack section") - - -def main() -> int: - errors: list[str] = [] - validate_links(errors) - validate_catalog(errors) - validate_version(errors) - validate_readme_contract(errors) - validate_public_content(errors) - validate_swift_format_configuration(errors) - validate_editor_configuration(errors) - validate_swift_format_script(errors) - validate_swift_format_guideline(errors) - validate_consumer_setup_script(errors) - validate_audit_skill(errors) - - if errors: - print("Guideline validation failed:") - for error in errors: - print(f"- {error}") - return 1 - - guide_count = len(list((ROOT / "Guidelines").rglob("*.md"))) - print(f"Validated {guide_count} guidelines for version {VERSION.read_text().strip()}.") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/AgentGuidelines/Scripts/validate_guidelines.swift b/AgentGuidelines/Scripts/validate_guidelines.swift new file mode 100755 index 0000000..39b58d6 --- /dev/null +++ b/AgentGuidelines/Scripts/validate_guidelines.swift @@ -0,0 +1,830 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() +let readme = root.appendingPathComponent("README.md") +let versionFile = root.appendingPathComponent("VERSION") +let changelog = root.appendingPathComponent("CHANGELOG.md") +let swiftFormatConfiguration = root.appendingPathComponent("Configurations/Swift/.swift-format") +let editorConfiguration = root.appendingPathComponent("Configurations/Swift/.editorconfig") +let swiftFormatScript = root.appendingPathComponent("Scripts/swift_format.sh") +let swiftFormatGuideline = root.appendingPathComponent("Guidelines/Swift/SwiftFormat.md") +let localizationGuideline = root.appendingPathComponent("Guidelines/Localization.md") +let appStoreGuideline = root.appendingPathComponent("Guidelines/AppStore.md") +let xcodeProjectSettingsGuideline = root.appendingPathComponent("Guidelines/Xcode/ProjectSettings.md") +let localizationPreparationScript = root.appendingPathComponent("Scripts/prepare_localizable_symbols.swift") +let localizationValidationScript = root.appendingPathComponent("Scripts/validate_string_catalogs.swift") +let consumerSetupScript = root.appendingPathComponent("Scripts/validate_consumer_setup.swift") +let testRunner = root.appendingPathComponent("Tests/run_tests.swift") +let auditSkill = root.appendingPathComponent(".agents/skills/agent-guidelines-audit/SKILL.md") +let markdownWrappingScript = root.appendingPathComponent( + ".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift" +) +let stringCatalogInspectionScript = root.appendingPathComponent( + ".agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift" +) +let developmentGuideline = root.appendingPathComponent("Guidelines/Development.md") +let cicdGuideline = root.appendingPathComponent("Guidelines/CICD.md") +let documentationGuideline = root.appendingPathComponent("Guidelines/Documentation.md") +let packagesGuideline = root.appendingPathComponent("Guidelines/Packages.md") +let agentsTemplate = root.appendingPathComponent("Templates/AGENTS.md") +let gitignoreTemplate = root.appendingPathComponent("Templates/.gitignore") +let gitignoreGuideline = root.appendingPathComponent("Guidelines/Git/IgnoreFiles.md") + +let expectedSwiftFormatRules: [String: Bool] = [ + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "AvoidRetroactiveConformances": true, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyLinesOpeningClosingBraces": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true, +] + +let markdownLinkPattern = #"\[[^\]]+\]\(([^)]+)\)"# +let semanticVersionPattern = + #"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"# +let forbiddenContent = [ + "/" + "Users" + "/": "personal absolute path", + "file" + "://": "local file URL", + "mobile-ios-" + "chauffeur": "work-repository identifier", + "black" + "lane": "work-repository identifier", +] +let upcomingFeatureSettings = [ + "SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE", + "SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN", + "SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION", + "SWIFT_UPCOMING_FEATURE_DYNAMIC_ACTOR_ISOLATION", + "SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY", + "SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES", + "SWIFT_UPCOMING_FEATURE_GLOBAL_ACTOR_ISOLATED_TYPES_USABILITY", + "SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY", + "SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS", + "SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS", + "SWIFT_UPCOMING_FEATURE_INFER_ISOLATED_CONFORMANCES", + "SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES", + "SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT", + "SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES", + "SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY", + "SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY", + "SWIFT_UPCOMING_FEATURE_NONISOLATED_NONSENDING_BY_DEFAULT", + "SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION", +] +let packageUpcomingFeatures = [ + "ExistentialAny", + "InferIsolatedConformances", + "InternalImportsByDefault", + "MemberImportVisibility", + "NonisolatedNonsendingByDefault", +] + +/// Returns all regular-expression matches in a string. +func matches(_ pattern: String, in value: String) -> [NSTextCheckingResult] { + guard let expression = try? NSRegularExpression(pattern: pattern) else { + return [] + } + return expression.matches(in: value, range: NSRange(value.startIndex.. String? { + let range = match.range(at: index) + guard range.location != NSNotFound, let swiftRange = Range(range, in: value) else { + return nil + } + return String(value[swiftRange]) +} + +/// Returns a repository-relative path. +func relativePath(_ url: URL) -> String { + url.standardizedFileURL.path.replacingOccurrences(of: root.standardizedFileURL.path + "/", with: "") +} + +/// Reads UTF-8 text or records an error. +func readText(_ url: URL, errors: inout [String]) -> String? { + do { + return try String(contentsOf: url, encoding: .utf8) + } catch { + errors.append("\(relativePath(url)): cannot read file: \(error.localizedDescription)") + return nil + } +} + +/// Recursively discovers regular files while excluding repository internals and build caches. +func recursiveFiles(below directory: URL) -> [URL] { + guard + let enumerator = FileManager.default.enumerator( + at: directory, + includingPropertiesForKeys: [.isRegularFileKey], + options: [] + ) + else { + return [] + } + var files: [URL] = [] + for case let url as URL in enumerator { + let relative = relativePath(url) + if relative.split(separator: "/").contains(where: { $0 == ".git" || $0 == ".build" || $0 == "__pycache__" }) { + if (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true { + enumerator.skipDescendants() + } + continue + } + if (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true { + files.append(url) + } + } + return files.sorted { $0.path < $1.path } +} + +/// Returns all public-text files governed by the privacy scan. +func textFiles() -> [URL] { + let suffixes: Set = ["md", "sh", "swift", "txt", "yml", "yaml"] + var files = recursiveFiles(below: root).filter { suffixes.contains($0.pathExtension.lowercased()) } + for url in [versionFile, root.appendingPathComponent("LICENSE"), swiftFormatConfiguration, editorConfiguration] + where FileManager.default.fileExists(atPath: url.path) { + files.append(url) + } + return Array(Set(files)).sorted { $0.path < $1.path } +} + +/// Resolves a relative Markdown link target within the source repository. +func resolveLink(source: URL, rawTarget: String) -> URL? { + var target = rawTarget.trimmingCharacters(in: .whitespacesAndNewlines) + .trimmingCharacters(in: CharacterSet(charactersIn: "<>")) + target = target.components(separatedBy: "#").first ?? target + if target.isEmpty || ["#", "http://", "https://", "mailto:"].contains(where: target.hasPrefix) { + return nil + } + let parts = NSString(string: target).pathComponents + if let index = parts.firstIndex(of: "AgentGuidelines") { + let suffix = parts.dropFirst(index + 1).joined(separator: "/") + return root.appendingPathComponent(suffix).standardizedFileURL + } + return source.deletingLastPathComponent().appendingPathComponent(target).standardizedFileURL +} + +/// Validates all relative Markdown link targets. +func validateLinks(_ errors: inout [String]) { + for source in recursiveFiles(below: root).filter({ $0.pathExtension.lowercased() == "md" }) { + guard let contents = readText(source, errors: &errors) else { + continue + } + for match in matches(markdownLinkPattern, in: contents) { + guard let rawTarget = capture(1, from: match, in: contents), + let resolved = resolveLink(source: source, rawTarget: rawTarget) + else { + continue + } + if !FileManager.default.fileExists(atPath: resolved.path) { + errors.append("\(relativePath(source)): missing link target '\(rawTarget)'") + } + } + } +} + +/// Validates that the README catalogs every shared guide. +func validateCatalog(_ errors: inout [String]) { + guard let contents = readText(readme, errors: &errors) else { + return + } + let guideRoot = root.appendingPathComponent("Guidelines") + for guide in recursiveFiles(below: guideRoot).filter({ $0.pathExtension.lowercased() == "md" }) { + let relative = relativePath(guide) + if !contents.contains("](\(relative))") { + errors.append("README.md: guideline is not cataloged: \(relative)") + } + } +} + +/// Validates the semantic version and matching changelog heading. +func validateVersion(_ errors: inout [String]) { + guard let version = readText(versionFile, errors: &errors)?.trimmingCharacters(in: .whitespacesAndNewlines) else { + return + } + if matches(semanticVersionPattern, in: version).isEmpty { + errors.append("VERSION: invalid semantic version '\(version)'") + } + if let contents = readText(changelog, errors: &errors), !contents.contains("## [\(version)]") { + errors.append("CHANGELOG.md: missing release heading for \(version)") + } +} + +/// Validates required README installation and integration contracts. +func validateReadmeContract(_ errors: inout [String]) { + guard let contents = readText(readme, errors: &errors) else { + return + } + if let version = readText(versionFile, errors: &errors)?.trimmingCharacters(in: .whitespacesAndNewlines), + contents.components(separatedBy: version).count - 1 != 2 + { + errors.append("README.md: installation and consumer-update commands must both use VERSION \(version)") + } + let required = [ + "alt=\"Xcode MCP\"": "Xcode MCP badge alt text", + "thatfactory/agent-guidelines/actions/workflows/ci.yml": "CI badge repository", + "--prefix=AgentGuidelines": "subtree destination", + "https://github.com/thatfactory/agent-guidelines.git": "subtree remote", + "git subtree add": "subtree installation command", + "git subtree pull": "subtree update command", + "AgentGuidelines/** linguist-generated": "generated subtree attribute", + "AgentGuidelines/Configurations/Swift/.swift-format": "swift-format symlink command", + "AgentGuidelines/Configurations/Swift/.editorconfig": "EditorConfig symlink command", + ".agents/skills/agent-guidelines-audit": "completion-audit skill setup", + "validate_consumer_setup.swift": "consumer setup validation command", + "--require-swift-format": "explicit Swift-format adoption validation", + "documentation-maintenance contract": "documentation contract synchronization", + "external-dependency contract": "external dependency contract synchronization", + "runtime-observability contract": "runtime observability contract synchronization", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("README.md: missing \(description): '\(value)'") + } +} + +/// Validates that public files contain no private or consumer-specific content. +func validatePublicContent(_ errors: inout [String]) { + for url in textFiles() { + guard let contents = readText(url, errors: &errors) else { + continue + } + for (forbidden, description) in forbiddenContent + where contents.localizedCaseInsensitiveContains(forbidden) { + errors.append("\(relativePath(url)): contains \(description): '\(forbidden)'") + } + } +} + +/// Returns whether two Foundation JSON values are equal. +func jsonEqual(_ lhs: Any?, _ rhs: Any?) -> Bool { + guard let lhs, let rhs else { + return lhs == nil && rhs == nil + } + return (lhs as AnyObject).isEqual(rhs) +} + +/// Validates the shared swift-format configuration. +func validateSwiftFormatConfiguration(_ errors: inout [String]) { + let configuration: [String: Any] + do { + let data = try Data(contentsOf: swiftFormatConfiguration) + guard let parsed = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw NSError( + domain: "GuidelineValidation", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "root value is not an object" + ]) + } + configuration = parsed + } catch { + errors.append("Configurations/Swift/.swift-format: invalid JSON: \(error.localizedDescription)") + return + } + let expectedValues: [String: Any] = [ + "indentation": ["spaces": 4], + "indentSwitchCaseLabels": false, + "lineLength": 120, + "tabWidth": 4, + "version": 1, + ] + for (key, expected) in expectedValues where !jsonEqual(configuration[key], expected) { + errors.append( + "Configurations/Swift/.swift-format: \(key) must be \(expected), found \(String(describing: configuration[key]))" + ) + } + let orderedImports = configuration["orderedImports"] as? [String: Any] + if orderedImports?["includeConditionalImports"] as? Bool != true { + errors.append( + "Configurations/Swift/.swift-format: orderedImports.includeConditionalImports must be true, found " + + String(describing: orderedImports?["includeConditionalImports"]) + ) + } + guard let rules = configuration["rules"] as? [String: Any], !rules.isEmpty else { + errors.append("Configurations/Swift/.swift-format: rules must be an exhaustive non-empty object") + return + } + let expectedKeys = Set(expectedSwiftFormatRules.keys) + let actualKeys = Set(rules.keys) + let missing = expectedKeys.subtracting(actualKeys).sorted() + let unexpected = actualKeys.subtracting(expectedKeys).sorted() + if !missing.isEmpty || !unexpected.isEmpty { + errors.append( + "Configurations/Swift/.swift-format: rule map mismatch; missing=\(missing), unexpected=\(unexpected)") + } + for rule in expectedKeys.intersection(actualKeys).sorted() { + let expected = expectedSwiftFormatRules[rule] ?? false + if rules[rule] as? Bool != expected { + errors.append( + "Configurations/Swift/.swift-format: \(rule) must be " + + "\(expected), found \(String(describing: rules[rule]))" + ) + } + } +} + +/// Validates the shared EditorConfig values. +func validateEditorConfiguration(_ errors: inout [String]) { + guard let contents = readText(editorConfiguration, errors: &errors) else { + return + } + let required = [ + "root = true", "[*.swift]", "indent_style = space", "indent_size = 4", "tab_width = 4", + "max_line_length = 120", "end_of_line = lf", "insert_final_newline = true", + "trim_trailing_whitespace = true", + ] + for value in required.sorted() where !contents.contains(value) { + errors.append("Configurations/Swift/.editorconfig: missing '\(value)'") + } +} + +/// Validates that an expected executable file exists. +func validateExecutable(_ url: URL, description: String, errors: inout [String]) { + guard FileManager.default.fileExists(atPath: url.path) else { + errors.append("\(relativePath(url)): missing \(description)") + return + } + if !FileManager.default.isExecutableFile(atPath: url.path) { + errors.append("\(relativePath(url)): \(description) is not executable") + } +} + +/// Validates native Swift ownership for repository automation. +func validateScriptLanguages(_ errors: inout [String]) { + let scriptsRoot = root.appendingPathComponent("Scripts") + let allowedShellPath = "Scripts/swift_format.sh" + for url in recursiveFiles(below: scriptsRoot) { + let relative = relativePath(url) + if url.pathExtension == "swift" || relative == allowedShellPath { + continue + } + errors.append("\(relative): repository scripts must use Swift; only \(allowedShellPath) is retained") + } + let auditScriptsRoot = root.appendingPathComponent(".agents/skills/agent-guidelines-audit/scripts") + for url in recursiveFiles(below: auditScriptsRoot) where url.pathExtension != "swift" { + errors.append("\(relativePath(url)): audit helper scripts must use Swift") + } + let testsRoot = root.appendingPathComponent("Tests") + for url in recursiveFiles(below: testsRoot) where url.pathExtension != "swift" { + errors.append("\(relativePath(url)): repository test automation must use Swift") + } + validateExecutable(testRunner, description: "native Swift test runner", errors: &errors) + for workflowPath in [".github/workflows/ci.yml", ".github/workflows/release.yml"] { + let workflow = root.appendingPathComponent(workflowPath) + guard let contents = readText(workflow, errors: &errors) else { continue } + for required in ["Tests/run_tests.swift", "Scripts/validate_guidelines.swift"] + where !contents.contains(required) { + errors.append("\(workflowPath): missing native validation command '\(required)'") + } + if contents.localizedCaseInsensitiveContains("python") { + errors.append("\(workflowPath): repository validation must not require Python") + } + } +} + +/// Validates the shared Swift-format workflow guide. +func validateSwiftFormatGuideline(_ errors: inout [String]) { + guard let contents = readText(swiftFormatGuideline, errors: &errors) else { + return + } + let required = [ + "## Swift package integration": "Swift package workflow", + "format-and-lint \\": "local package formatting command", + "Package.swift": "package manifest formatting scope", + "## CI integration": "CI workflow", + "lint-strict \\": "strict CI command", + "Never run `format` or `format-and-lint` in CI": "non-mutating CI rule", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Swift/SwiftFormat.md: missing \(description): '\(value)'") + } +} + +/// Validates the documentation-maintenance policy. +func validateDocumentationGuideline(_ errors: inout [String]) { + guard let contents = readText(documentationGuideline, errors: &errors) else { + return + } + let required = [ + "Documentation is part of implementation": "implementation-time documentation rule", + "Regardless of change size": "existing-document staleness rule", + "inaccurate, incomplete, misleading, or obsolete": "stale documentation criteria", + "A small change that leaves durable knowledge and existing documentation accurate": + "minor-change documentation churn guardrail", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Documentation.md: missing \(description): '\(value)'") + } +} + +/// Validates the generated-symbol localization workflow. +func validateLocalizationGuideline(_ errors: inout [String]) { + guard let contents = readText(localizationGuideline, errors: &errors) else { + return + } + let required = [ + "using-generated-localizable-symbols-in-your-code": "Apple generated-symbol reference", + "localizing-your-app-using-agents": "Apple agent-localization reference", + "Xcode-generated `LocalizedStringResource` symbols": "generated-symbol default", + "prepare_localizable_symbols.swift": "shared symbol-preparation workflow", + "validate_string_catalogs.swift": "shared catalog-validation workflow", + "stale extracted entry": "stale-entry policy", + "marked `new` or `needs_review`": "translation-state policy", + "placeholder positions, semantic names, and conversion types": "format-signature policy", + "product voice, terminology": "consumer-specific translation boundary", + "small repository-owned wrapper": "consumer configuration boundary", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Localization.md: missing \(description): '\(value)'") + } +} + +/// Validates native reusable localization scripts. +func validateLocalizationScripts(_ errors: inout [String]) { + let scripts: [(URL, [String])] = [ + (localizationPreparationScript, ["prepareCatalog", "symbolIssues", "--check"]), + ( + localizationValidationScript, + [ + "--catalog-directory", "--source-directory", "--required-language", "formatSignatureIssues", + "translationStateIssues", "literalLocalizationReferences", + ] + ), + ] + for (script, requiredValues) in scripts { + validateExecutable(script, description: "localization script", errors: &errors) + guard let contents = readText(script, errors: &errors) else { + continue + } + if contents.contains("Headroom") { + errors.append("\(relativePath(script)): contains consumer-specific logic") + } + for value in requiredValues where !contents.contains(value) { + errors.append("\(relativePath(script)): missing localization behavior '\(value)'") + } + } +} + +/// Validates the Xcode project-settings contract. +func validateXcodeProjectSettingsGuideline(_ errors: inout [String]) { + guard let contents = readText(xcodeProjectSettingsGuideline, errors: &errors) else { + return + } + let required = [ + "https://developer.apple.com/documentation/xcode/build-settings-reference": + "official Apple build-settings reference", + "GCC_TREAT_WARNINGS_AS_ERRORS": "C and Objective-C warning policy", + "MTL_TREAT_WARNINGS_AS_ERRORS": "Metal warning policy", + "SWIFT_TREAT_WARNINGS_AS_ERRORS": "Swift warning policy", + "SWIFT_APPROACHABLE_CONCURRENCY = YES": "approachable concurrency baseline", + "SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor": "default actor isolation baseline", + "SWIFT_STRICT_CONCURRENCY = complete": "strict concurrency baseline", + "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO": "export-compliance build-setting baseline", + "ITSAppUsesNonExemptEncryption`](https://developer.apple.com/": "official export-compliance key reference", + "built `Info.plist`": "built export-compliance verification", + "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = YES": "documented non-exempt encryption exception", + "newest stable Swift language mode": "future-facing Swift language policy", + "Enable each feature that remains opt-in": "language-mode-aware upcoming-feature policy", + "warnings-as-errors can turn that diagnostic into a build failure": "redundant upcoming-feature safety rule", + "Xcode 27 inventory to evaluate": "Xcode 27 upcoming-feature inventory", + "project-level `.xcconfig`": "project-level configuration ownership", + "unit-test and UI-test targets": "test-target effective-value audit", + "nearest applicable `AGENTS.md`": "local exception source", + "condition for removing or revisiting the exception": "exception lifecycle", + "## Swift package parity": "Swift package parity section", + "../Packages.md#compiler-settings-baseline": "Swift package baseline cross-reference", + "Whenever this Xcode project baseline adds, removes, or changes a Swift or Clang compiler policy": + "package applicability maintenance rule", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Xcode/ProjectSettings.md: missing \(description): '\(value)'") + } + for setting in upcomingFeatureSettings where !contents.contains(setting) { + errors.append( + "Guidelines/Xcode/ProjectSettings.md: missing Xcode 27 upcoming-feature inventory entry: '\(setting)'") + } +} + +/// Validates the Swift package compiler-settings contract. +func validatePackageCompilerSettingsGuideline(_ errors: inout [String]) { + guard let contents = readText(packagesGuideline, errors: &errors) else { + return + } + let required = [ + "## Compiler settings baseline": "compiler-settings baseline section", + "Xcode/ProjectSettings.md": "Xcode baseline cross-reference", + "swiftLanguageModes": "package-level Swift language mode", + ".treatAllWarnings(as: .error)": "typed warnings-as-errors policy", + "Swift 6 language mode enables complete concurrency checking unconditionally": + "Swift 6 strict-concurrency explanation", + ".enableUpcomingFeature(\"StrictConcurrency\")": + "upcoming StrictConcurrency redundancy rule", + ".enableExperimentalFeature(\"StrictConcurrency\")": + "experimental StrictConcurrency redundancy rule", + "StrictConcurrency=complete": "explicit StrictConcurrency redundancy rule", + "`.defaultIsolation(MainActor.self)` is intentionally not part": + "default MainActor isolation exclusion", + "package plug-in targets for which `Target.plugin(...)` does not expose `swiftSettings`": + "unsupported plug-in target exclusion", + "Retain no redundant upcoming features": "redundant upcoming-feature prohibition", + "condition for revisiting or removing the exception": "package-setting exception lifecycle", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Packages.md: missing package compiler policy \(description): '\(value)'") + } + for feature in packageUpcomingFeatures where !contents.contains(".enableUpcomingFeature(\"\(feature)\")") { + errors.append("Guidelines/Packages.md: missing required SwiftPM upcoming feature '\(feature)'") + } +} + +/// Validates the shared App Store metadata workflow. +func validateAppStoreGuideline(_ errors: inout [String]) { + guard let contents = readText(appStoreGuideline, errors: &errors) else { + return + } + let required = [ + "app-store-connect-mcp": "MCP source", + "AppStore/": "repository metadata root", + "validate_repository": "repository validation", + "plan_metadata_changes": "metadata planning", + "plan_screenshot_changes": "screenshot planning", + "apply_plan": "immutable plan application", + "get_operation_status": "uncertain-outcome recovery", + "noOp: true": "remote reconciliation", + ".appstore-connect-mcp/": "ignored operational state", + "does not authorize submitting": "submission boundary", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/AppStore.md: missing \(description): '\(value)'") + } +} + +/// Validates the native-first dependency policy. +func validateExternalDependencyPolicy(_ errors: inout [String]) { + if let contents = readText(developmentGuideline, errors: &errors) { + let required = [ + "## External dependencies": "external dependency policy section", + "Do not introduce a new third-party source or binary dependency": "native and first-party default", + "explicit approval from the repository owner": "repository-owner approval gate", + "durable repository documentation": "durable exception record", + "Tooling dependencies explicitly required by these shared guidelines": "tooling-only exception", + "CocoaPods and Carthage are forbidden": "forbidden package managers", + "Use Swift Package Manager for package dependencies": "Swift Package Manager requirement", + "## Post-merge cleanup": "post-merge cleanup section", + "fast-forward-only pull": "safe primary-branch update", + "delete the merged local feature branch": "merged local branch removal", + "Confirm the remote feature branch is absent": "remote branch cleanup verification", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Development.md: missing \(description): '\(value)'") + } + } + if let contents = readText(cicdGuideline, errors: &errors) { + let required = [ + "## Tooling and automation": "CI/CD tooling policy section", + "## Private repository dependencies": "private repository dependency authentication section", + "Fastlane is forbidden": "forbidden delivery tooling", + "xcode-cloud-mcp": "first-party Xcode Cloud tooling", + "app-store-connect-mcp": "first-party App Store tooling", + "required behavior cannot be implemented": "non-Swift capability-gap threshold", + "missing Swift capability": "documented non-Swift exception", + "actions/create-github-app-token@v3": "short-lived GitHub App token workflow", + "client-id:": "current GitHub App client identifier input", + "permission-contents: read": "read-only private dependency permission", + "GIT_CONFIG_KEY_0": "process-level Git authentication", + "GIT_CONFIG_VALUE_0: https://github.com/": "GitHub HTTPS rewrite source", + "complete subprocess tree as privileged": "credential-bearing subprocess trust boundary", + "isolated disposable or ephemeral self-hosted runner": "untrusted-code runner isolation", + "This trust rule is event-independent": "event-independent credential boundary", + "pull_request_target": "untrusted pull-request credential boundary", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/CICD.md: missing \(description): '\(value)'") + } + } + if let contents = readText(packagesGuideline, errors: &errors) { + let required = [ + "[external dependency policy](Development.md#external-dependencies)": "package dependency policy pointer", + "must not introduce or conceal a third-party runtime dependency": "first-party package boundary", + "guideline-mandated tooling dependency": "DocC tooling exception", + "root `LICENSE` file": "root license-file retention", + "Retain the README license badge": "README license-badge retention", + "do not add a dedicated License heading or license-description section": + "README license-section prohibition", + ] + for (value, description) in required where !contents.contains(value) { + errors.append("Guidelines/Packages.md: missing \(description): '\(value)'") + } + } + if let contents = readText(agentsTemplate, errors: &errors), + !contents.contains("BEGIN THATFACTORY EXTERNAL DEPENDENCY CONTRACT v1") + { + errors.append("Templates/AGENTS.md: missing external-dependency contract") + } +} + +/// Validates the completion-audit skill and native helper scripts. +func validateAuditSkill(_ errors: inout [String]) { + guard let contents = readText(auditSkill, errors: &errors) else { + return + } + let required = [ + "name: agent-guidelines-audit": "skill name", + "before claiming completion": "completion trigger", + "git diff --check": "diff validation", + "validate_consumer_setup.swift": "consumer integration validation", + "format-and-lint": "local Swift-format audit", + "lint-strict": "strict Swift-format CI audit", + "AppLogger": "AppLogger integration audit", + "Logging.md": "shared Logging guide reference", + "Dependency declaration and target linkage alone": "lifecycle observability coverage audit", + "## Audit documentation consistency": "documentation drift audit", + "Known stale documentation blocks completion": "stale documentation stopping rule", + "## Audit documentation formatting": "documentation formatting audit", + "check_markdown_wrapping.swift": "Markdown line-wrapping check", + "existing root Markdown files and declared durable documentation folders": + "documentation-convention adoption pass", + "new third-party dependency": "third-party dependency audit", + "repository-owner approval": "third-party dependency approval gate", + "no unresolved P0/P1 blocker remains": "Codex review stopping rule", + "## Audit Xcode project settings": "Xcode project-settings audit", + "Xcode/ProjectSettings.md": "shared Xcode project-settings guide reference", + "GCC_TREAT_WARNINGS_AS_ERRORS": "warnings-as-errors project audit", + "SWIFT_UPCOMING_FEATURE_": "future-facing upcoming-feature audit", + "SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor": "actor-isolation project audit", + "SWIFT_VERSION": "Swift language-version project audit", + "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO": "export-compliance project audit", + "## Audit App Store metadata": "App Store metadata audit", + "noOp: true": "App Store remote reconciliation audit", + "xcodebuild -showBuildSettings": "effective target build-setting inspection", + "nearest applicable `AGENTS.md`": "project-setting exception lookup", + "## Audit localization": "localization audit", + "Guidelines/Localization.md": "shared Localization guide reference", + "prepare_localizable_symbols.swift": "generated-symbol preparation audit", + "validate_string_catalogs.swift": "String Catalog validation audit", + "check_xcstrings_inspection.swift": "String Catalog editor evidence gate", + "Fail closed when a changed catalog lacks that recorded editor evidence": + "missing String Catalog editor evidence stopping rule", + "pull-request description": "durable pull-request evidence summary", + "A local wrapper may": "consumer localization-wrapper boundary", + "new repository-owned executable scripts": "native Swift script audit", + "Templates/.gitignore": "shared gitignore template comparison", + "# Project-specific: ": "project-specific gitignore exception marker", + "git check-ignore -v": "gitignore behavior verification", + "git ls-files": "tracked-file safety check", + "CocoaPods or Carthage": "forbidden package-manager audit", + "Guidelines/CICD.md": "shared CI/CD guide reference", + "fastlane adoption": "forbidden delivery-tooling audit", + "missing Swift capability": "non-Swift script exception audit", + "## Audit Swift package settings": "Swift package-settings audit", + "Guidelines/Packages.md#compiler-settings-baseline": "shared package compiler-settings guide reference", + "Package.swift": "package manifest discovery", + "swift package --package-path dump-package": "evaluated manifest inspection", + "swiftLanguageModes": "package language-mode audit", + ".treatAllWarnings(as: .error)": "package warnings-as-errors audit", + ".enableUpcomingFeature(...)": "package upcoming-feature audit", + "complete strict concurrency as supplied by the language mode": "Swift 6 strict-concurrency handling", + ".enableUpcomingFeature(\"StrictConcurrency\")": "upcoming StrictConcurrency removal audit", + ".enableExperimentalFeature(\"StrictConcurrency\")": "experimental StrictConcurrency removal audit", + "StrictConcurrency=complete": "explicit StrictConcurrency removal audit", + ".defaultIsolation(MainActor.self)": "default MainActor isolation exclusion", + "package plug-in targets": "unsupported plug-in target exclusion", + "package-setting failure": "package-setting exception lookup", + ] + for (value, description) in required where !contents.contains(value) { + errors.append(".agents/skills/agent-guidelines-audit/SKILL.md: missing \(description): '\(value)'") + } + for feature in packageUpcomingFeatures where !contents.contains(feature) { + errors.append(".agents/skills/agent-guidelines-audit/SKILL.md: missing package feature audit '\(feature)'") + } + validateExecutable(markdownWrappingScript, description: "Markdown wrapping checker", errors: &errors) + validateExecutable(stringCatalogInspectionScript, description: "String Catalog inspection checker", errors: &errors) + if let development = readText(developmentGuideline, errors: &errors), + !development.contains("$agent-guidelines-audit") + { + errors.append("Guidelines/Development.md: missing mandatory $agent-guidelines-audit invocation") + } + if let template = readText(agentsTemplate, errors: &errors) { + let requiredTemplateValues = [ + "AgentGuidelines/Guidelines/Development.md": "Development.md pointer", + "BEGIN THATFACTORY DOCUMENTATION MAINTENANCE CONTRACT v1": "documentation-maintenance contract", + "BEGIN THATFACTORY EXTERNAL DEPENDENCY CONTRACT v1": "external-dependency contract", + "BEGIN THATFACTORY RUNTIME OBSERVABILITY CONTRACT v1": "runtime-observability contract", + "AgentGuidelines/Guidelines/Documentation.md": "Documentation.md pointer", + "## Stack": "Stack section", + ] + for (value, description) in requiredTemplateValues where !template.contains(value) { + errors.append("Templates/AGENTS.md: missing \(description)") + } + } +} + +/// Validates the reusable ignore template and its shared reconciliation policy. +func validateGitignoreGuidance(_ errors: inout [String]) { + guard let template = readText(gitignoreTemplate, errors: &errors) else { return } + let activePatterns = template.split(separator: "\n").map(String.init) + let requiredPatterns = [ + ".DS_Store", "xcuserdata/", "/build/", "/DerivedData/", "/.build/", "/Packages/", + ".swiftpm/configuration/registries.json", "node_modules/", "dist/", "coverage/", "__pycache__/", + ".venv/", ".env", "!.env.example", ".netrc", ".appstore-connect-mcp/", ".devspace/", + "/public-check/", + ] + for pattern in requiredPatterns where !activePatterns.contains(pattern) { + errors.append("Templates/.gitignore: missing required pattern '\(pattern)'") + } + for forbidden in ["Package.resolved", "*.xcodeproj", "*.xcworkspace", ".swiftpm/"] + where activePatterns.contains(forbidden) { + errors.append("Templates/.gitignore: must not ignore authored or lockfile path '\(forbidden)'") + } + for forbiddenPrefix in ["Carthage/", "fastlane/", "Pods/"] + where activePatterns.contains(where: { $0.hasPrefix(forbiddenPrefix) }) { + errors.append("Templates/.gitignore: must not include forbidden tooling pattern '\(forbiddenPrefix)'") + } + guard let guideline = readText(gitignoreGuideline, errors: &errors) else { return } + for required in [ + "active `.gitignore` patterns", "# Project-specific: ", "git check-ignore -v", "git ls-files", + "Do not remove it or silently accept it", + ] where !guideline.contains(required) { + errors.append("Guidelines/Git/IgnoreFiles.md: missing ignore reconciliation policy '\(required)'") + } +} + +/// Runs every repository guideline validation. +func main() -> Int32 { + var errors: [String] = [] + validateLinks(&errors) + validateCatalog(&errors) + validateVersion(&errors) + validateReadmeContract(&errors) + validatePublicContent(&errors) + validateSwiftFormatConfiguration(&errors) + validateEditorConfiguration(&errors) + validateExecutable(swiftFormatScript, description: "Swift-format script", errors: &errors) + validateScriptLanguages(&errors) + validateSwiftFormatGuideline(&errors) + validateDocumentationGuideline(&errors) + validateLocalizationGuideline(&errors) + validateAppStoreGuideline(&errors) + validateLocalizationScripts(&errors) + validateXcodeProjectSettingsGuideline(&errors) + validatePackageCompilerSettingsGuideline(&errors) + validateExternalDependencyPolicy(&errors) + validateGitignoreGuidance(&errors) + validateExecutable(consumerSetupScript, description: "consumer setup validator", errors: &errors) + validateAuditSkill(&errors) + if !errors.isEmpty { + print("Guideline validation failed:") + for error in errors { print("- \(error)") } + return 1 + } + let guideRoot = root.appendingPathComponent("Guidelines") + let guideCount = recursiveFiles(below: guideRoot).filter { $0.pathExtension.lowercased() == "md" }.count + let version = + (try? String(contentsOf: versionFile, encoding: .utf8))? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "unknown" + print("Validated \(guideCount) guidelines for version \(version).") + return 0 +} + +exit(main()) diff --git a/AgentGuidelines/Scripts/validate_string_catalogs.swift b/AgentGuidelines/Scripts/validate_string_catalogs.swift new file mode 100755 index 0000000..c92dd38 --- /dev/null +++ b/AgentGuidelines/Scripts/validate_string_catalogs.swift @@ -0,0 +1,421 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +/// One normalized printf placeholder. +struct FormatToken: Hashable { + let position: Int + let name: String? + let format: String +} + +/// Parsed validation command-line values. +struct Arguments { + var catalogDirectories: [String] = [] + var sourceDirectories: [String] = [] + var symbolCatalogs: [String] = [] + var requiredLanguages: Set = [] +} + +let formatSpecifierPattern = + #"%(?!%)(?:([1-9]\d*)\$)?(?:\(([A-Za-z_][A-Za-z0-9_]*)\))?([-+ #0']*(?:\d+|\*)?(?:\.(?:\d+|\*))?(?:hh|h|ll|l|q|L|z|t|j)?[diouxXfFeEgGaAcCsSp@])"# + +/// Returns one capture from a regular-expression match. +func capture(_ index: Int, from match: NSTextCheckingResult, in value: String) -> String? { + let range = match.range(at: index) + guard range.location != NSNotFound, let swiftRange = Range(range, in: value) else { + return nil + } + return String(value[swiftRange]) +} + +/// Returns position, semantic name, and type for every printf placeholder. +func formatSignature(_ value: String) -> [FormatToken] { + guard let expression = try? NSRegularExpression(pattern: formatSpecifierPattern) else { + return [] + } + let range = NSRange(value.startIndex.. FormatToken in + defer { implicitPosition += 1 } + let position = capture(1, from: match, in: value).flatMap(Int.init) ?? implicitPosition + return FormatToken( + position: position, + name: capture(2, from: match, in: value), + format: capture(3, from: match, in: value) ?? "" + ) + } + return tokens.sorted { + ($0.position, $0.name ?? "", $0.format) < ($1.position, $1.name ?? "", $1.format) + } +} + +/// Returns every leaf String Catalog value below one localization. +func stringUnitValues(_ value: Any) -> [String] { + if let dictionary = value as? [String: Any] { + var values: [String] = [] + if let stringUnit = dictionary["stringUnit"] as? [String: Any], + let stringValue = stringUnit["value"] as? String + { + values.append(stringValue) + } + for (key, child) in dictionary where key != "stringUnit" { + values.append(contentsOf: stringUnitValues(child)) + } + return values + } + if let array = value as? [Any] { + return array.flatMap(stringUnitValues) + } + return [] +} + +/// Returns every leaf String Catalog state below one localization. +func stringUnitStates(_ value: Any) -> [String] { + if let dictionary = value as? [String: Any] { + var states: [String] = [] + if let stringUnit = dictionary["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String + { + states.append(state) + } + for (key, child) in dictionary where key != "stringUnit" { + states.append(contentsOf: stringUnitStates(child)) + } + return states + } + if let array = value as? [Any] { + return array.flatMap(stringUnitStates) + } + return [] +} + +/// Renders a normalized placeholder signature for diagnostics. +func displaySignature(_ signature: [FormatToken]) -> String { + let specifiers = signature.map { "%" + ($0.name.map { "(\($0))" } ?? "") + $0.format } + return specifiers.isEmpty ? "none" : specifiers.joined(separator: ", ") +} + +/// Returns translated values whose placeholder signature differs from source. +func formatSignatureIssues(_ catalog: [String: Any]) -> [String] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, + let strings = catalog["strings"] as? [String: Any] + else { + return ["catalog structure is invalid"] + } + var issues: [String] = [] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any], + entry["extractionState"] as? String != "stale", + let localizations = entry["localizations"] as? [String: Any] + else { + continue + } + let sourceSignatures = Set(stringUnitValues(localizations[sourceLanguage] as Any).map(formatSignature)) + guard sourceSignatures.count == 1, let expected = sourceSignatures.first else { + issues.append("\(key): source variants have inconsistent format specifiers") + continue + } + for language in localizations.keys.sorted() where language != sourceLanguage { + for value in stringUnitValues(localizations[language] as Any) { + let actual = formatSignature(value) + if actual != expected { + issues.append( + "\(key) [\(language)]: format specifiers \(displaySignature(actual)) " + + "do not match \(displaySignature(expected))" + ) + } + } + } + } + return issues +} + +/// Returns missing required localizations and unfinished translated values. +func translationStateIssues(_ catalog: [String: Any], requiredLanguages: Set) -> [String] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, + let strings = catalog["strings"] as? [String: Any] + else { + return ["catalog structure is invalid"] + } + var issues: [String] = [] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any], entry["extractionState"] as? String != "stale" else { + continue + } + let localizations = entry["localizations"] as? [String: Any] ?? [:] + for language in requiredLanguages.subtracting([sourceLanguage]).sorted() where localizations[language] == nil { + issues.append("\(key) [\(language)]: required localization is missing") + } + for language in localizations.keys.sorted() where language != sourceLanguage { + let states = stringUnitStates(localizations[language] as Any) + if states.isEmpty { + issues.append("\(key) [\(language)]: localization has no string units") + continue + } + let unfinished = Set(states.filter { $0 == "new" || $0 == "needs_review" }).sorted() + if !unfinished.isEmpty { + issues.append("\(key) [\(language)]: unfinished states \(unfinished.joined(separator: ", "))") + } + } + } + return issues +} + +/// Returns active catalog entries that cannot generate expected Swift symbols. +func symbolIssues(_ catalog: [String: Any]) -> [String] { + guard let sourceLanguage = catalog["sourceLanguage"] as? String, + let strings = catalog["strings"] as? [String: Any] + else { + return ["catalog structure is invalid"] + } + var issues: [String] = [] + for key in strings.keys.sorted() { + guard let entry = strings[key] as? [String: Any] else { + issues.append("\(key): entry is not a dictionary") + continue + } + if entry["extractionState"] as? String == "stale" { + continue + } + if entry["extractionState"] as? String != "manual" { + issues.append("\(key): extractionState is not manual") + } + let localizations = entry["localizations"] as? [String: Any] + if localizations?[sourceLanguage] as? [String: Any] == nil { + issues.append("\(key): source localization \(sourceLanguage) is missing") + } + } + return issues +} + +/// Recursively discovers files with an extension below a directory. +func files(withExtension pathExtension: String, below directories: [String]) -> [String] { + let fileManager = FileManager.default + var paths = Set() + for directory in directories { + guard let enumerator = fileManager.enumerator(atPath: directory) else { + continue + } + for case let candidate as String in enumerator where candidate.hasSuffix(".\(pathExtension)") { + let path = URL(fileURLWithPath: directory).appendingPathComponent(candidate).standardizedFileURL.path + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: path, isDirectory: &isDirectory), !isDirectory.boolValue { + paths.insert(path) + } + } + } + return paths.sorted() +} + +/// Runs a process and returns its standard output. +func run(_ command: [String]) throws -> Data { + let process = Process() + let output = Pipe() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = command + process.standardOutput = output + process.standardError = output + try process.run() + let outputData = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + guard process.terminationStatus == 0 else { + let detail = + String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + ?? "xcstringstool extract failed" + throw NSError( + domain: "CatalogValidation", code: Int(process.terminationStatus), + userInfo: [ + NSLocalizedDescriptionKey: detail + ]) + } + return outputData +} + +/// Returns checked-in Swift locations that still use localization literals. +func literalLocalizationReferences(sourceDirectories: [String]) throws -> [String] { + let sourcePaths = files(withExtension: "swift", below: sourceDirectories) + if sourcePaths.isEmpty { + return [] + } + let temporary = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: temporary, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: temporary) } + _ = try run( + [ + "xcrun", "xcstringstool", "extract", "--modern-localizable-strings", "--SwiftUI", + "--omit-empty-stringsdata", "--output-directory", temporary.path, + ] + sourcePaths + ) + let stringsDataPaths = files(withExtension: "stringsdata", below: [temporary.path]) + var references: [String] = [] + for path in stringsDataPaths { + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + guard let stringsData = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + continue + } + let source = stringsData["source"] as? String ?? URL(fileURLWithPath: path).lastPathComponent + let tables = stringsData["tables"] as? [String: Any] + let entries = tables?["Localizable"] as? [[String: Any]] ?? [] + for entry in entries { + let location = entry["location"] as? [String: Any] + let line: String + if let number = location?["startingLine"] as? NSNumber { + line = number.stringValue + } else { + line = "?" + } + let key = entry["key"] as? String ?? "" + references.append("\(source):\(line): \(key)") + } + } + return references.sorted() +} + +/// Parses validation command-line arguments. +func parseArguments(_ values: [String]) throws -> Arguments { + var arguments = Arguments() + var index = 0 + while index < values.count { + let value = values[index] + if value == "--help" { + print( + "Usage: validate_string_catalogs.swift --catalog-directory " + + "--source-directory [--symbol-catalog ] " + + "[--required-language ]" + ) + exit(0) + } + guard index + 1 < values.count else { + throw NSError( + domain: "CatalogValidation", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "missing value for \(value)" + ]) + } + let next = values[index + 1] + switch value { + case "--catalog-directory": arguments.catalogDirectories.append(next) + case "--source-directory": arguments.sourceDirectories.append(next) + case "--symbol-catalog": arguments.symbolCatalogs.append(URL(fileURLWithPath: next).standardizedFileURL.path) + case "--required-language": arguments.requiredLanguages.insert(next) + default: + throw NSError( + domain: "CatalogValidation", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "unknown argument: \(value)" + ]) + } + index += 2 + } + guard !arguments.catalogDirectories.isEmpty, !arguments.sourceDirectories.isEmpty else { + throw NSError( + domain: "CatalogValidation", code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "--catalog-directory and --source-directory are required" + ]) + } + return arguments +} + +/// Writes text to standard error. +func writeError(_ value: String) { + FileHandle.standardError.write(Data((value + "\n").utf8)) +} + +/// Validates configured catalogs and Swift source directories. +func main() -> Int32 { + do { + let arguments = try parseArguments(Array(CommandLine.arguments.dropFirst())) + let catalogPaths = files(withExtension: "xcstrings", below: arguments.catalogDirectories) + guard !catalogPaths.isEmpty else { + writeError("No String Catalogs found in the configured directories.") + return 1 + } + let catalogSet = Set(catalogPaths) + let symbolCatalogs = + arguments.symbolCatalogs.isEmpty + ? Set(catalogPaths.filter { URL(fileURLWithPath: $0).lastPathComponent == "Localizable.xcstrings" }) + : Set(arguments.symbolCatalogs) + let unknownSymbolCatalogs = symbolCatalogs.subtracting(catalogSet) + if !unknownSymbolCatalogs.isEmpty { + for path in unknownSymbolCatalogs.sorted() { + writeError("\(path): generated-symbol catalog is outside the configured catalogs.") + } + return 1 + } + var failed = false + for catalogPath in catalogPaths { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: catalogPath)) + guard let catalog = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let strings = catalog["strings"] as? [String: Any] + else { + writeError("\(catalogPath): catalog has no strings dictionary.") + failed = true + continue + } + let staleKeys = strings.keys.filter { + (strings[$0] as? [String: Any])?["extractionState"] as? String == "stale" + }.sorted() + if !staleKeys.isEmpty { + failed = true + writeError("\(catalogPath): stale extracted strings:") + for key in staleKeys { writeError(" - \(key)") } + } + if symbolCatalogs.contains(catalogPath) { + let issues = symbolIssues(catalog) + if !issues.isEmpty { + failed = true + writeError("\(catalogPath): entries that are not symbol-ready:") + for issue in issues { writeError(" - \(issue)") } + } + } + let formatIssues = formatSignatureIssues(catalog) + if !formatIssues.isEmpty { + failed = true + writeError("\(catalogPath): format-specifier mismatches:") + for issue in formatIssues { writeError(" - \(issue)") } + } + let stateIssues = translationStateIssues(catalog, requiredLanguages: arguments.requiredLanguages) + if !stateIssues.isEmpty { + failed = true + writeError("\(catalogPath): localization-state issues:") + for issue in stateIssues { writeError(" - \(issue)") } + } + } catch { + writeError("\(catalogPath): \(error.localizedDescription)") + failed = true + } + } + do { + let references = try literalLocalizationReferences(sourceDirectories: arguments.sourceDirectories) + if !references.isEmpty { + failed = true + writeError("Checked-in Swift localization literals must use generated symbols:") + for reference in references { writeError(" - \(reference)") } + } + } catch { + writeError("Could not validate Swift localization literals: \(error.localizedDescription)") + return 1 + } + if failed { + writeError( + "Prepare generated-symbol catalogs, migrate reported Swift literals, and resolve catalog issues." + ) + return 1 + } + print("String Catalog validation passed: symbols, states, format signatures, and Swift source are valid.") + return 0 + } catch { + writeError("String Catalog validation failed: \(error.localizedDescription)") + return 2 + } +} + +exit(main()) diff --git a/AgentGuidelines/Templates/.gitignore b/AgentGuidelines/Templates/.gitignore new file mode 100644 index 0000000..3a3fc11 --- /dev/null +++ b/AgentGuidelines/Templates/.gitignore @@ -0,0 +1,66 @@ +# macOS +.DS_Store + +# Xcode user data, build output, and archives +xcuserdata/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.hmap +/build/ +/DerivedData/ +*.ipa +*.dSYM +*.dSYM.zip + +# Xcode playground generated state +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +/.build/ +/Packages/ +.swiftpm/configuration/registries.json + +# Node.js +node_modules/ +dist/ +coverage/ +*.tgz +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Local credentials and environment configuration +.env +.env.* +!.env.example +!.env.*.example +.netrc + +# ThatFactory tooling state +.appstore-connect-mcp/ +.devspace/ +/public-check/ + +# Project-specific entries require a durable rationale after repository-owner review. +# Project-specific: diff --git a/AgentGuidelines/Templates/AGENTS.md b/AgentGuidelines/Templates/AGENTS.md index 2728920..092c1a9 100644 --- a/AgentGuidelines/Templates/AGENTS.md +++ b/AgentGuidelines/Templates/AGENTS.md @@ -13,7 +13,7 @@ Read only the guides relevant to the task: - [Swift style](AgentGuidelines/Guidelines/Swift/SwiftStyle.md) - [SwiftUI](AgentGuidelines/Guidelines/Swift/SwiftUI.md) - [Swift format](AgentGuidelines/Guidelines/Swift/SwiftFormat.md) -- [Localization](AgentGuidelines/Guidelines/Swift/Localization.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) @@ -23,14 +23,59 @@ Read only the guides relevant to the task: - [Git repositories and SSH-first cloning](AgentGuidelines/Guidelines/Git/Repositories.md) - [GitHub pull requests](AgentGuidelines/Guidelines/GitHub/PullRequests.md) - [Xcode MCP and visual verification](AgentGuidelines/Guidelines/Xcode/MCP.md) +- [Xcode project settings](AgentGuidelines/Guidelines/Xcode/ProjectSettings.md) - [Xcode security audits](AgentGuidelines/Guidelines/Xcode/Security.md) For an application that uses Redux, also read [Redux architecture](AgentGuidelines/Guidelines/Architecture/Redux.md). -Keep the following marked 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. +Keep the following observability contract in the consumer repository's root `AGENTS.md` so implementation agents treat runtime diagnostics as part of lifecycle work. Copy it unchanged and update it when the marker version changes in this template. ```md - + +## Runtime Observability + +Treat privacy-safe runtime observability as part of implementing or changing stateful, asynchronous, fallible, or lifecycle-oriented behavior. Identify the meaningful success, failure, cancellation, recovery, and state-transition boundaries before handoff, and emit concise AppLogger events owned by the artifact that implements them. Dependency declaration or target linkage alone does not satisfy this requirement. + +Every ThatFactory package log starts with its canonical emoji and uses its own stable subsystem. Never log credentials, account or record identifiers, share URLs, captured content, images, or other user-generated values as public metadata. Keep pure values and utilities silent when they have no meaningful diagnostic event; record that deliberate decision in the implementation handoff instead of adding initializer or property-access noise. Follow [Logging](AgentGuidelines/Guidelines/Logging.md) for ownership, privacy, severity, message design, and tests. + +``` + +Keep the following marked external-dependency contract in the consumer repository's root `AGENTS.md` so implementation agents receive the rule directly before they make dependency choices. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## External Dependency Policy + +Do not introduce third-party source or binary dependencies into ThatFactory applications, games, or reusable packages during normal development. Prefer Apple platform APIs, the Swift standard library, code owned by the current repository, or focused ThatFactory-owned packages. If reusable capability is missing, implement it natively at the appropriate boundary and consider extracting it into a first-party package instead of selecting an external library. + +A third-party dependency may be added, or expanded to a new target or runtime role, only with explicit repository-owner approval for that specific use before modifying the dependency graph. Convenience, reduced implementation effort, popularity, or an agent's preference for an existing library are not sufficient justification. Do not make an external library acceptable merely by hiding it behind a first-party wrapper. + +Document every approved exception in durable repository documentation in the same change. Record the dependency and source, purpose and target scope, why a native or first-party implementation is not appropriate, relevant license, security, and maintenance considerations, and the approval context. Merely mentioning or using the dependency in an execution plan, pull-request description, or transient chat is not approval. Regardless of where explicit approval occurs, reflect the exception in durable repository documentation. + +Apple system frameworks and the Swift standard library are not third-party dependencies. ThatFactory-owned packages are first-party dependencies. Tooling explicitly required by the shared guidelines is allowed only for its documented tooling role and must not be linked into or shipped with product runtime targets unless separately approved and documented. + +Follow [Development workflow](AgentGuidelines/Guidelines/Development.md) for the detailed policy. + +``` + +Keep the following documentation-maintenance contract in the consumer repository's root `AGENTS.md` so implementation agents receive it directly rather than only through a linked guide. Copy it unchanged and update it when the marker version changes in this template. + +```md + +## Documentation Maintenance + +Treat documentation as part of implementation, not optional follow-up. At the start of implementation, identify code-level or project-level documentation likely to describe the affected behavior; before handoff, reconcile that documentation with the final implementation. + +Update documentation when a change alters durable or core feature behavior or another documented contract. Regardless of change size, if the implementation makes existing documentation inaccurate, incomplete, misleading, or obsolete, update or remove that documentation in the same change. + +Do not create documentation churn for incidental implementation details that are not durable and do not affect an existing documented claim. Follow [Documentation](AgentGuidelines/Guidelines/Documentation.md) for detailed scope and the completion checklist. + +``` + +Keep the following marked code-review contract in the consumer repository's root `AGENTS.md` so it is loaded directly for root-level Codex and pull-request work. Copy it unchanged and update it when the marker version changes in this template; a Markdown link to the detailed workflow is not an instruction include. + +```md + ## Code Review Rules Review for release-blocking defects introduced or materially exposed by the pull request. A clean review means no unresolved P0/P1 findings; it does not mean exhaustive or perfect software. @@ -43,8 +88,10 @@ Treat P2/P3 observations as non-blocking, including defense-in-depth, theoretica 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. -Automatic Codex review is the initial review. Do not request a manual Codex review unless the repository owner explicitly asks. Never request another review after each remediation commit. Within the normal review budget, at most one owner-authorized, delta-scoped verification review may be requested under [the pull-request review workflow](AgentGuidelines/Guidelines/GitHub/PullRequests.md). - +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 @@ -67,7 +114,7 @@ Replace these examples with exact repository paths: ## Stack -Record the supported Xcode, Swift, and platform versions. State strict-concurrency mode, default actor isolation, infer-isolated-conformance behavior, and `nonisolated(nonsending)` defaults when they apply. Clarify whether application, package, and test targets share those settings. +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 diff --git a/AgentGuidelines/Tests/run_tests.swift b/AgentGuidelines/Tests/run_tests.swift new file mode 100755 index 0000000..847eeab --- /dev/null +++ b/AgentGuidelines/Tests/run_tests.swift @@ -0,0 +1,911 @@ +#!/usr/bin/env swift +import Foundation + +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + +struct CommandResult { + let status: Int32 + let output: String + + var succeeded: Bool { status == 0 } +} + +struct TestFailure: Error, CustomStringConvertible { + let description: String +} + +let repositoryRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() +let fileManager = FileManager.default + +func require(_ condition: @autoclosure () -> Bool, _ message: String) throws { + guard condition() else { throw TestFailure(description: message) } +} + +func run(_ arguments: [String], directory: URL = repositoryRoot) throws -> CommandResult { + let process = Process() + let output = Pipe() + process.currentDirectoryURL = directory + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = arguments + process.standardOutput = output + process.standardError = output + try process.run() + let data = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + return CommandResult(status: process.terminationStatus, output: String(decoding: data, as: UTF8.self)) +} + +func withTemporaryDirectory(_ body: (URL) throws -> Void) throws { + let directory = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: directory) } + try body(directory) +} + +func write(_ value: String, to url: URL) throws { + try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) + try value.write(to: url, atomically: true, encoding: .utf8) +} + +func script(_ path: String) -> String { + repositoryRoot.appendingPathComponent(path).path +} + +func copyRepositoryFixture(to destination: URL) throws { + try fileManager.createDirectory(at: destination, withIntermediateDirectories: true) + let children = try fileManager.contentsOfDirectory(at: repositoryRoot, includingPropertiesForKeys: nil) + for child in children where ![".git", ".build"].contains(child.lastPathComponent) { + try fileManager.copyItem(at: child, to: destination.appendingPathComponent(child.lastPathComponent)) + } +} + +func git(_ arguments: String..., in directory: URL) throws { + let result = try run(["git"] + arguments, directory: directory) + try require(result.succeeded, "git \(arguments.joined(separator: " ")) failed: \(result.output)") +} + +func initializeRepository(at root: URL) throws { + try git("init", in: root) + try git("config", "user.email", "agent@example.com", in: root) + try git("config", "user.name", "Agent", in: root) +} + +func createConsumerFixture(at root: URL, isPackage: Bool, readme: String? = nil) throws { + try fileManager.createSymbolicLink( + at: root.appendingPathComponent("AgentGuidelines"), withDestinationURL: repositoryRoot) + try fileManager.copyItem( + at: repositoryRoot.appendingPathComponent("Templates/AGENTS.md"), + to: root.appendingPathComponent("AGENTS.md")) + try write("AgentGuidelines/** linguist-generated\n", to: root.appendingPathComponent(".gitattributes")) + let skillParent = root.appendingPathComponent(".agents/skills") + try fileManager.createDirectory(at: skillParent, withIntermediateDirectories: true) + try fileManager.createSymbolicLink( + at: skillParent.appendingPathComponent("agent-guidelines-audit"), + withDestinationURL: repositoryRoot.appendingPathComponent(".agents/skills/agent-guidelines-audit") + ) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".swift-format"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.swift-format")) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".editorconfig"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.editorconfig")) + try write( + """ + name: CI + on: + pull_request: + push: + branches: [main] + jobs: + swift-format: + steps: + - run: AgentGuidelines/Scripts/swift_format.sh lint-strict Package.swift Sources Tests + """ + "\n", + to: root.appendingPathComponent(".github/workflows/ci.yml") + ) + if isPackage { + try write("// swift-tools-version: 6.0\n", to: root.appendingPathComponent("Package.swift")) + } + if let readme { + try write(readme, to: root.appendingPathComponent("README.md")) + } +} + +func localization(_ value: String, state: String = "translated") -> String { + #"{"stringUnit":{"state":"\#(state)","value":"\#(value)"}}"# +} + +let tests: [(String, () throws -> Void)] = [ + ( + "repository validator accepts the source tree", + { + let result = try run([script("Scripts/validate_guidelines.swift")]) + try require(result.succeeded, result.output) + try require(result.output.contains("Validated"), "validator did not report success") + } + ), + ( + "repository validator rejects configuration drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let configuration = fixture.appendingPathComponent("Configurations/Swift/.swift-format") + var contents = try String(contentsOf: configuration, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "\"NeverForceUnwrap\" : false", with: "\"NeverForceUnwrap\" : true") + try require(contents.contains("\"NeverForceUnwrap\" : true"), "could not mutate fixture configuration") + try write(contents, to: configuration) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "configuration drift unexpectedly passed") + try require(result.output.contains("NeverForceUnwrap must be false"), result.output) + } + } + ), + ( + "repository validator rejects missing package compiler policy", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".treatAllWarnings(as: .error)", with: ".warningsAsErrors()") + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing package warning policy unexpectedly passed") + try require(result.output.contains("missing package compiler policy"), result.output) + } + } + ), + ( + "repository validator rejects package upcoming-feature drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".enableUpcomingFeature(\"NonisolatedNonsendingByDefault\")", + with: ".enableUpcomingFeature(\"ExampleFeature\")" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "package upcoming-feature drift unexpectedly passed") + try require( + result.output.contains( + "missing required SwiftPM upcoming feature 'NonisolatedNonsendingByDefault'"), + result.output + ) + } + } + ), + ( + "repository validator rejects missing package plug-in exclusion", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "package plug-in targets for which `Target.plugin(...)` does not expose `swiftSettings`", + with: "other package targets" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing package plug-in exclusion unexpectedly passed") + try require( + result.output.contains("missing package compiler policy unsupported plug-in target exclusion"), + result.output) + } + } + ), + ( + "repository validator rejects missing experimental StrictConcurrency rule", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".enableExperimentalFeature(\"StrictConcurrency\")", + with: ".enableExperimentalFeature(\"ExampleFeature\")" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing experimental StrictConcurrency rule unexpectedly passed") + try require( + result.output.contains( + "missing package compiler policy experimental StrictConcurrency redundancy rule"), result.output + ) + } + } + ), + ( + "repository validator rejects missing upcoming StrictConcurrency rule", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: ".enableUpcomingFeature(\"StrictConcurrency\")", + with: ".enableUpcomingFeature(\"ExampleFeature\")" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing upcoming StrictConcurrency rule unexpectedly passed") + try require( + result.output.contains( + "missing package compiler policy upcoming StrictConcurrency redundancy rule"), result.output + ) + } + } + ), + ( + "repository validator rejects missing explicit StrictConcurrency rule", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Packages.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "StrictConcurrency=complete", + with: "StrictConcurrency=minimal" + ) + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing explicit StrictConcurrency rule unexpectedly passed") + try require( + result.output.contains( + "missing package compiler policy explicit StrictConcurrency redundancy rule"), result.output + ) + } + } + ), + ( + "repository validator rejects missing Xcode package-parity maintenance", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/Xcode/ProjectSettings.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "../Packages.md#compiler-settings-baseline", with: "package-policy") + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing Xcode package-parity maintenance unexpectedly passed") + try require(result.output.contains("missing Swift package baseline cross-reference"), result.output) + } + } + ), + ( + "repository validator rejects missing package audit section", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let skill = fixture.appendingPathComponent(".agents/skills/agent-guidelines-audit/SKILL.md") + var contents = try String(contentsOf: skill, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "## Audit Swift package settings", with: "## Inspect Swift manifests") + try write(contents, to: skill) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing package audit section unexpectedly passed") + try require(result.output.contains("missing Swift package-settings audit"), result.output) + } + } + ), + ( + "repository validator rejects package audit behavior drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let skill = fixture.appendingPathComponent(".agents/skills/agent-guidelines-audit/SKILL.md") + var contents = try String(contentsOf: skill, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "swift package --package-path dump-package", + with: "inspect the manifest" + ) + try write(contents, to: skill) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "package audit behavior drift unexpectedly passed") + try require(result.output.contains("missing evaluated manifest inspection"), result.output) + } + } + ), + ( + "repository validator rejects missing observability adoption guidance", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let readme = fixture.appendingPathComponent("README.md") + var contents = try String(contentsOf: readme, encoding: .utf8) + contents = contents.replacingOccurrences(of: "runtime-observability contract", with: "runtime contract") + try write(contents, to: readme) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "missing observability adoption guidance unexpectedly passed") + try require( + result.output.contains("missing runtime observability contract synchronization"), result.output) + } + } + ), + ( + "repository validator rejects private dependency authentication drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guideline = fixture.appendingPathComponent("Guidelines/CICD.md") + var contents = try String(contentsOf: guideline, encoding: .utf8) + contents = contents.replacingOccurrences( + of: "isolated disposable or ephemeral self-hosted runner", + with: "self-hosted runner") + try write(contents, to: guideline) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "private dependency authentication drift unexpectedly passed") + try require( + result.output.contains("missing untrusted-code runner isolation"), + result.output + ) + } + } + ), + ( + "repository validator rejects gitignore template drift", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let template = fixture.appendingPathComponent("Templates/.gitignore") + var contents = try String(contentsOf: template, encoding: .utf8) + contents = contents.replacingOccurrences(of: "node_modules/\n", with: "") + try write(contents, to: template) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "gitignore template drift unexpectedly passed") + try require( + result.output.contains("Templates/.gitignore: missing required pattern 'node_modules/'"), + result.output + ) + } + } + ), + ( + "repository validator rejects forbidden tooling in gitignore template", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let template = fixture.appendingPathComponent("Templates/.gitignore") + var contents = try String(contentsOf: template, encoding: .utf8) + contents += "fastlane/test_output/\n" + try write(contents, to: template) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "forbidden tooling pattern unexpectedly passed") + try require( + result.output.contains("must not include forbidden tooling pattern 'fastlane/'"), + result.output + ) + } + } + ), + ( + "Markdown checker reports governed wrapping", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Example.md") + try write( + """ + # Example + + This paragraph was split + across physical lines. + + - This list item was split + across physical lines too. + + > This quotation was split + > across physical lines as well. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(!result.succeeded, "wrapped prose unexpectedly passed") + for expected in [ + ":3: paragraph spans physical lines 3-4", + ":6: list item spans physical lines 6-7", + ":9: block quote (depth 1) spans physical lines 9-10", + ] { + try require(result.output.contains(expected), "missing diagnostic: \(expected)\n\(result.output)") + } + } + } + ), + ( + "Markdown checker accepts GitHub alerts", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Alerts.md") + try write( + """ + # Alerts + + > [!NOTE] + > Note body. + + > [!TIP] + > Tip body. + + > [!IMPORTANT] + > Important body. + + > [!WARNING] + > Warning body. + + > [!CAUTION] + > Caution body. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "Markdown checker rejects wrapped or malformed GitHub alerts", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Alerts.md") + try write( + """ + # Alerts + + > [!NOTE] + > This alert body was split + > across physical lines. + + > [!UNKNOWN] + > Unknown alert body. + + > > [!TIP] + > > Nested alert body. + + > Ordinary quote begins. + > [!NOTE] + > Ordinary quote continues. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(!result.succeeded, "invalid alerts unexpectedly passed") + for expected in [ + ":4: block quote (depth 1) spans physical lines 4-5", + ":7: block quote (depth 1) spans physical lines 7-8", + ":10: block quote (depth 2) spans physical lines 10-11", + ":13: block quote (depth 1) spans physical lines 13-15", + ] { + try require(result.output.contains(expected), "missing diagnostic: \(expected)\n\(result.output)") + } + } + } + ), + ( + "Markdown checker accepts verbatim structures", + { + try withTemporaryDirectory { root in + let markdown = root.appendingPathComponent("Example.md") + try write( + """ + --- + title: Example + --- + + # Example + + One physical line of prose. + + | Role | Folder | + |---|---| + | App | `App/` | + + ```text + diagram + ``` + +

+ Badge +

+ + - Parent item. + - Nested item. + """ + "\n", + to: markdown + ) + let result = try run([ + script(".agents/skills/agent-guidelines-audit/scripts/check_markdown_wrapping.swift"), + markdown.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "String Catalog inspection fails closed for every Git change kind", + { + try withTemporaryDirectory { root in + try initializeRepository(at: root) + for name in ["Modified.xcstrings", "DeletedThenRenamed.xcstrings", "Copied.xcstrings"] { + try write("{}\n", to: root.appendingPathComponent(name)) + } + try git("add", ".", in: root) + try git("commit", "-m", "Fixture", in: root) + try write(#"{"sourceLanguage":"en"}"# + "\n", to: root.appendingPathComponent("Modified.xcstrings")) + try git("mv", "DeletedThenRenamed.xcstrings", "Renamed.xcstrings", in: root) + try fileManager.copyItem( + at: root.appendingPathComponent("Copied.xcstrings"), + to: root.appendingPathComponent("Added.xcstrings") + ) + try git("add", "Added.xcstrings", in: root) + try write("{}\n", to: root.appendingPathComponent("Untracked.xcstrings")) + let result = try run( + [ + script(".agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift"), + "--repository", root.path, "--base-ref", "HEAD", + ] + ) + try require(!result.succeeded, "missing editor evidence unexpectedly passed") + for name in ["Added.xcstrings", "Modified.xcstrings", "Renamed.xcstrings", "Untracked.xcstrings"] { + try require( + result.output.contains("missing Xcode catalog-editor inspection evidence: \(name)"), + "missing changed catalog \(name): \(result.output)") + } + try require(result.output.contains("--evidence-output is required"), result.output) + } + } + ), + ( + "String Catalog inspection records structured zero-diagnostic evidence", + { + try withTemporaryDirectory { temporary in + let root = temporary.appendingPathComponent("repository") + try fileManager.createDirectory(at: root, withIntermediateDirectories: true) + try initializeRepository(at: root) + let catalog = root.appendingPathComponent("Localizable.xcstrings") + try write("{}\n", to: catalog) + try git("add", ".", in: root) + try git("commit", "-m", "Fixture", in: root) + try write(#"{"sourceLanguage":"en"}"# + "\n", to: catalog) + let evidence = temporary.appendingPathComponent("evidence.json") + let result = try run( + [ + script(".agents/skills/agent-guidelines-audit/scripts/check_xcstrings_inspection.swift"), + "--repository", root.path, "--base-ref", "HEAD", "--inspected-catalog", + "Localizable.xcstrings", "--evidence-output", evidence.path, + ] + ) + try require(result.succeeded, result.output) + let data = try Data(contentsOf: evidence) + let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] + let catalogs = object?["catalogs"] as? [[String: Any]] + try require(object?["baseRef"] as? String == "HEAD", "base ref was not recorded") + try require( + (object?["xcodeVersion"] as? String)?.contains("Xcode") == true, "Xcode version was not recorded") + try require( + catalogs?.first?["path"] as? String == "Localizable.xcstrings", "catalog path was not recorded") + try require(catalogs?.first?["catalogEditorWarnings"] as? Int == 0, "warning count was not zero") + try require(catalogs?.first?["catalogEditorErrors"] as? Int == 0, "error count was not zero") + } + } + ), + ( + "symbol preparation preserves translations and stale entries", + { + try withTemporaryDirectory { root in + let catalog = root.appendingPathComponent("Localizable.xcstrings") + try write( + """ + {"sourceLanguage":"en","strings":{ + "Legacy value":{"comment":"Visible title","extractionState":"extracted_with_value","localizations":{"de":\(localization("Alter Wert"))}}, + "Old value":{"extractionState":"stale","localizations":{"en":\(localization("Old"))}} + },"version":"1.1"} + """ + "\n", + to: catalog + ) + let check = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path, "--check"]) + try require(!check.succeeded, "unprepared catalog unexpectedly passed") + let preparation = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path]) + try require(preparation.succeeded, preparation.output) + let data = try Data(contentsOf: catalog) + let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] + let strings = object?["strings"] as? [String: Any] + let active = strings?["Legacy value"] as? [String: Any] + let activeLocalizations = active?["localizations"] as? [String: Any] + let stale = strings?["Old value"] as? [String: Any] + try require(active?["extractionState"] as? String == "manual", "active key was not made manual") + try require(active?["comment"] as? String == "Visible title", "comment was not preserved") + try require( + activeLocalizations?["de"] != nil && activeLocalizations?["en"] != nil, + "translations were not preserved") + try require(stale?["extractionState"] as? String == "stale", "stale key was revived") + let finalCheck = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path, "--check"]) + try require(finalCheck.succeeded, finalCheck.output) + } + } + ), + ( + "symbol preparation rejects manual entries without source copy", + { + try withTemporaryDirectory { root in + let catalog = root.appendingPathComponent("Localizable.xcstrings") + try write( + #"{"sourceLanguage":"en","strings":{"semanticKey":{"extractionState":"manual","localizations":{"de":{"stringUnit":{"state":"translated","value":"Wert"}}}}}}"#, + to: catalog + ) + let result = try run([script("Scripts/prepare_localizable_symbols.swift"), catalog.path]) + try require(!result.succeeded, "invalid manual key unexpectedly passed") + try require(result.output.contains("manual entry has no en source value"), result.output) + } + } + ), + ( + "String Catalog validator accepts valid symbols, states, and formats", + { + try withTemporaryDirectory { root in + let catalogs = root.appendingPathComponent("Catalogs") + let sources = root.appendingPathComponent("Sources") + try fileManager.createDirectory(at: sources, withIntermediateDirectories: true) + try write( + """ + {"sourceLanguage":"en","strings":{"summary":{"extractionState":"manual","localizations":{ + "en":\(localization("%1$(count)lld items")), + "de":\(localization("%1$(count)lld Einträge", state: "machine_translated")) + }}}} + """ + "\n", + to: catalogs.appendingPathComponent("Localizable.xcstrings") + ) + let result = try run( + [ + script("Scripts/validate_string_catalogs.swift"), "--catalog-directory", catalogs.path, + "--source-directory", sources.path, "--required-language", "de", + ] + ) + try require(result.succeeded, result.output) + } + } + ), + ( + "String Catalog validator reports independent catalog defects", + { + try withTemporaryDirectory { root in + let catalogs = root.appendingPathComponent("Catalogs") + let sources = root.appendingPathComponent("Sources") + try fileManager.createDirectory(at: sources, withIntermediateDirectories: true) + try write( + """ + {"sourceLanguage":"en","strings":{ + "count":{"extractionState":"manual","localizations":{"en":\(localization("%1$(count)lld items")),"de":\(localization("%1$(value)@ Einträge", state: "needs_review"))}}, + "Old":{"extractionState":"stale"} + }} + """ + "\n", + to: catalogs.appendingPathComponent("Localizable.xcstrings") + ) + let result = try run( + [ + script("Scripts/validate_string_catalogs.swift"), "--catalog-directory", catalogs.path, + "--source-directory", sources.path, "--required-language", "de", "--required-language", "fr", + ] + ) + try require(!result.succeeded, "invalid catalog unexpectedly passed") + for expected in [ + "stale extracted strings", "format specifiers", "required localization is missing", + "unfinished states needs_review", + ] { + try require(result.output.contains(expected), "missing diagnostic \(expected): \(result.output)") + } + } + } + ), + ( + "consumer validator rejects package README License headings", + { + for readme in [ + "# Package\n\n## License\n", "# Package\n\n#### lIcEnSe ####\n", + "# Package\n\nLicense\n-------\n", "# Package\n\nLICENSE\n=======\n", + ] { + try withTemporaryDirectory { root in + try createConsumerFixture(at: root, isPackage: true, readme: readme) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(!result.succeeded, "package License heading unexpectedly passed") + try require(result.output.contains("must not contain a dedicated License heading"), result.output) + } + } + } + ), + ( + "consumer validator rejects standalone package README license prose", + { + try withTemporaryDirectory { root in + try createConsumerFixture( + at: root, + isPackage: true, + readme: "# Example\n\nExample is available under the MIT license. See [LICENSE](LICENSE).\n" + ) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(!result.succeeded, "standalone package license prose unexpectedly passed") + try require( + result.output.contains("must not contain a standalone license-description paragraph"), + result.output + ) + } + } + ), + ( + "consumer validator accepts package license badge and absent README", + { + try withTemporaryDirectory { root in + try createConsumerFixture( + at: root, + isPackage: true, + readme: """ + # Example + + ![License](https://img.shields.io/badge/License-MIT-green.svg) + + See [LICENSE](LICENSE) when verifying redistribution terms. + + ````markdown + ```markdown + ## License section example + + Example is available under the MIT license. See [LICENSE](LICENSE). + ``` + ```` + + ~~~text + # License + ~~~ + """ + "\n" + ) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(result.succeeded, result.output) + } + try withTemporaryDirectory { root in + try createConsumerFixture(at: root, isPackage: true) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "consumer validator limits README license policy to package roots", + { + try withTemporaryDirectory { root in + try createConsumerFixture(at: root, isPackage: false, readme: "# App\n\n## License\n") + try write( + "# License\n\nStandalone documentation may discuss licensing.\n", + to: root.appendingPathComponent("Documentation/README.md") + ) + let result = try run([ + script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path, + ]) + try require(result.succeeded, result.output) + } + } + ), + ( + "consumer validator accepts synchronized integration", + { + try withTemporaryDirectory { root in + try fileManager.createSymbolicLink( + at: root.appendingPathComponent("AgentGuidelines"), withDestinationURL: repositoryRoot) + try fileManager.copyItem( + at: repositoryRoot.appendingPathComponent("Templates/AGENTS.md"), + to: root.appendingPathComponent("AGENTS.md")) + try write("AgentGuidelines/** linguist-generated\n", to: root.appendingPathComponent(".gitattributes")) + let skillParent = root.appendingPathComponent(".agents/skills") + try fileManager.createDirectory(at: skillParent, withIntermediateDirectories: true) + try fileManager.createSymbolicLink( + at: skillParent.appendingPathComponent("agent-guidelines-audit"), + withDestinationURL: repositoryRoot.appendingPathComponent(".agents/skills/agent-guidelines-audit") + ) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".swift-format"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.swift-format") + ) + try fileManager.createSymbolicLink( + at: root.appendingPathComponent(".editorconfig"), + withDestinationURL: repositoryRoot.appendingPathComponent("Configurations/Swift/.editorconfig") + ) + try write( + """ + name: CI + on: + pull_request: + push: + branches: [main] + jobs: + swift-format: + steps: + - run: AgentGuidelines/Scripts/swift_format.sh lint-strict Sources + """ + "\n", + to: root.appendingPathComponent(".github/workflows/ci.yml") + ) + let result = try run([script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path]) + try require(result.succeeded, result.output) + } + } + ), + ( + "consumer validator rejects copied audit skill and contract drift", + { + try withTemporaryDirectory { root in + try fileManager.createSymbolicLink( + at: root.appendingPathComponent("AgentGuidelines"), withDestinationURL: repositoryRoot) + let template = try String( + contentsOf: repositoryRoot.appendingPathComponent("Templates/AGENTS.md"), encoding: .utf8) + try write( + template.replacingOccurrences( + of: "P0/P1", with: "P0", options: [], range: template.range(of: "P0/P1")), + to: root.appendingPathComponent("AGENTS.md")) + try write("*.md text\n", to: root.appendingPathComponent(".gitattributes")) + let skill = root.appendingPathComponent(".agents/skills/agent-guidelines-audit") + try fileManager.createDirectory(at: skill, withIntermediateDirectories: true) + try write("stale copy\n", to: skill.appendingPathComponent("SKILL.md")) + let result = try run([script("Scripts/validate_consumer_setup.swift"), "--consumer-root", root.path]) + try require(!result.succeeded, "invalid consumer unexpectedly passed") + for expected in ["code-review contract does not match", "linguist-generated", "must be a symlink"] { + try require(result.output.contains(expected), "missing diagnostic \(expected): \(result.output)") + } + } + } + ), +] + +var failures = 0 +for (name, test) in tests { + do { + try test() + print("PASS \(name)") + } catch { + failures += 1 + FileHandle.standardError.write(Data("FAIL \(name): \(error)\n".utf8)) + } +} + +if failures > 0 { + FileHandle.standardError.write(Data("\(failures) of \(tests.count) tests failed.\n".utf8)) + exit(1) +} + +print("All \(tests.count) native Swift tests passed.") diff --git a/AgentGuidelines/Tests/test_validate_consumer_setup.py b/AgentGuidelines/Tests/test_validate_consumer_setup.py deleted file mode 100644 index 4db91af..0000000 --- a/AgentGuidelines/Tests/test_validate_consumer_setup.py +++ /dev/null @@ -1,253 +0,0 @@ -"""Tests for consumer agent-guidelines integration validation.""" - -from __future__ import annotations - -import importlib.util -import tempfile -import unittest -from pathlib import Path - - -VALIDATOR_PATH = ( - Path(__file__).resolve().parents[1] / "Scripts" / "validate_consumer_setup.py" -) -SPEC = importlib.util.spec_from_file_location("validate_consumer_setup", 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 ConsumerSetupTests(unittest.TestCase): - """Verifies checked-in consumer guidance and symlink wiring.""" - - def setUp(self) -> None: - self.temporary_directory = tempfile.TemporaryDirectory(dir=VALIDATOR.GUIDELINES_ROOT) - self.consumer_root = Path(self.temporary_directory.name) - (self.consumer_root / "AgentGuidelines").symlink_to( - VALIDATOR.GUIDELINES_ROOT, - target_is_directory=True, - ) - - template = ( - VALIDATOR.GUIDELINES_ROOT / "Templates" / "AGENTS.md" - ).read_text(encoding="utf-8") - contract_errors: list[str] = [] - contract = VALIDATOR.extract_contract(template, contract_errors, "template") - self.assertEqual(contract_errors, []) - self.assertIsNotNone(contract) - - agents = f"""# Project Instructions - -{contract} - -## Codex review scope - -For consumer pull requests, verify exact tagged-tree provenance before excluding -`AgentGuidelines/**`, and confirm the required `.gitattributes` rule. -""" - (self.consumer_root / "AGENTS.md").write_text(agents, encoding="utf-8") - (self.consumer_root / ".gitattributes").write_text( - "AgentGuidelines/** linguist-generated\n", - encoding="utf-8", - ) - - skill_parent = self.consumer_root / ".agents" / "skills" - skill_parent.mkdir(parents=True) - (skill_parent / "agent-guidelines-audit").symlink_to( - VALIDATOR.GUIDELINES_ROOT - / ".agents" - / "skills" - / "agent-guidelines-audit", - target_is_directory=True, - ) - - def tearDown(self) -> None: - self.temporary_directory.cleanup() - - def validate(self, require_swift_format: bool = False) -> list[str]: - errors: list[str] = [] - VALIDATOR.validate_consumer_setup( - errors, - self.consumer_root, - require_swift_format=require_swift_format, - ) - return errors - - def adopt_swift_format(self) -> None: - agents_path = self.consumer_root / "AGENTS.md" - agents = agents_path.read_text(encoding="utf-8") - agents_path.write_text( - f"{agents}\n[Swift format]({VALIDATOR.SWIFT_FORMAT_GUIDE})\n", - encoding="utf-8", - ) - - def add_swift_format_links(self) -> None: - (self.consumer_root / ".swift-format").symlink_to( - VALIDATOR.GUIDELINES_ROOT / "Configurations" / "Swift" / ".swift-format" - ) - (self.consumer_root / ".editorconfig").symlink_to( - VALIDATOR.GUIDELINES_ROOT / "Configurations" / "Swift" / ".editorconfig" - ) - - def add_package(self) -> None: - (self.consumer_root / "Package.swift").write_text( - "// swift-tools-version: 6.0\n", - encoding="utf-8", - ) - (self.consumer_root / "Sources").mkdir() - (self.consumer_root / "Tests").mkdir() - - def add_strict_format_ci(self, paths: tuple[str, ...] = ()) -> None: - workflows = self.consumer_root / ".github" / "workflows" - workflows.mkdir(parents=True) - command = " AgentGuidelines/Scripts/swift_format.sh lint-strict" - if paths: - path_lines = [] - for index, path in enumerate(paths): - continuation = " \\" if index < len(paths) - 1 else "" - path_lines.append(f" {path}{continuation}") - command += " \\\n" + "\n".join(path_lines) - (workflows / "ci-pr.yml").write_text( - f"""name: CI (PR) -on: - pull_request: - push: - branches: [main] -jobs: - swift-format: - name: Swift Format - steps: - - name: Run strict swift-format lint - run: | -{command} -""", - encoding="utf-8", - ) - - def test_valid_consumer_setup(self) -> None: - """Accepts a synchronized root contract and repository skill symlink.""" - self.assertEqual(self.validate(), []) - - def test_rejects_stale_code_review_contract(self) -> None: - """Rejects a consumer root contract that drifted from the template.""" - agents_path = self.consumer_root / "AGENTS.md" - agents = agents_path.read_text(encoding="utf-8") - agents_path.write_text(agents.replace("P0/P1", "P0", 1), encoding="utf-8") - - self.assertTrue( - any("code-review contract does not match" in error for error in self.validate()) - ) - - def test_rejects_copied_audit_skill(self) -> None: - """Rejects a stale-copy risk in place of the repository symlink.""" - skill = self.consumer_root / ".agents" / "skills" / "agent-guidelines-audit" - skill.unlink() - skill.mkdir() - (skill / "SKILL.md").write_text("stale copy\n", encoding="utf-8") - - self.assertTrue(any("must be a symlink" in error for error in self.validate())) - - def test_requires_generated_attribute(self) -> None: - """Rejects a consumer without the collapsed subtree diff rule.""" - (self.consumer_root / ".gitattributes").write_text("*.md text\n", encoding="utf-8") - - self.assertTrue(any("linguist-generated" in error for error in self.validate())) - - def test_rejects_missing_local_agent_link(self) -> None: - """Rejects a local guideline pointer that does not resolve.""" - agents_path = self.consumer_root / "AGENTS.md" - agents = agents_path.read_text(encoding="utf-8") - agents_path.write_text( - f"{agents}\n[Missing guide](AgentGuidelines/Guidelines/Missing.md)\n", - encoding="utf-8", - ) - - self.assertTrue( - any("missing local link target" in error for error in self.validate()) - ) - - def test_explicitly_requires_formatter_adoption(self) -> None: - """Allows an audit to require formatting before AGENTS.md links the guide.""" - self.assertEqual(self.validate(), []) - - errors = self.validate(require_swift_format=True) - - self.assertTrue(any("consumer .swift-format" in error for error in errors)) - self.assertTrue(any("consumer .editorconfig" in error for error in errors)) - self.assertTrue(any("Swift format CI" in error for error in errors)) - - def test_adopted_formatter_requires_links_and_ci(self) -> None: - """Detects incomplete adoption from the consumer AGENTS.md link.""" - self.adopt_swift_format() - - errors = self.validate() - - self.assertTrue(any("consumer .swift-format" in error for error in errors)) - self.assertTrue(any("consumer .editorconfig" in error for error in errors)) - self.assertTrue(any("Swift format CI" in error for error in errors)) - - def test_accepts_adopted_package_formatter(self) -> None: - """Accepts package configuration links and complete strict CI coverage.""" - self.adopt_swift_format() - self.add_swift_format_links() - self.add_package() - self.add_strict_format_ci(("Package.swift", "Sources", "Tests")) - - self.assertEqual(self.validate(), []) - - def test_rejects_mutating_ci_formatter(self) -> None: - """Rejects source rewriting inside CI even when strict lint is also present.""" - self.adopt_swift_format() - self.add_swift_format_links() - self.add_strict_format_ci() - workflow = self.consumer_root / ".github" / "workflows" / "ci-pr.yml" - workflow.write_text( - workflow.read_text(encoding="utf-8") - + " AgentGuidelines/Scripts/swift_format.sh format Sources\n", - encoding="utf-8", - ) - - self.assertTrue(any("must not mutate sources" in error for error in self.validate())) - - def test_rejects_ci_without_required_triggers(self) -> None: - """Requires strict lint for pull requests and main-branch merges.""" - self.adopt_swift_format() - self.add_swift_format_links() - self.add_strict_format_ci() - workflow = self.consumer_root / ".github" / "workflows" / "ci-pr.yml" - contents = workflow.read_text(encoding="utf-8") - workflow.write_text( - contents.replace(" pull_request:\n", "").replace( - " push:\n branches: [main]\n", - "", - ), - encoding="utf-8", - ) - - errors = self.validate() - - self.assertTrue(any("does not run for pull requests" in error for error in errors)) - self.assertTrue(any("does not run for pushes to main" in error for error in errors)) - - def test_rejects_incomplete_package_ci_scope(self) -> None: - """Requires all standard package Swift roots in strict CI.""" - self.adopt_swift_format() - self.add_swift_format_links() - self.add_package() - self.add_strict_format_ci(("Sources",)) - workflow = self.consumer_root / ".github" / "workflows" / "ci-pr.yml" - workflow.write_text( - workflow.read_text(encoding="utf-8") - + " - run: echo Package.swift Tests\n", - encoding="utf-8", - ) - - errors = self.validate() - - self.assertTrue(any("'Package.swift'" in error for error in errors)) - self.assertTrue(any("'Tests'" in error for error in errors)) - - -if __name__ == "__main__": - unittest.main() diff --git a/AgentGuidelines/Tests/test_validate_guidelines.py b/AgentGuidelines/Tests/test_validate_guidelines.py deleted file mode 100644 index 1216b37..0000000 --- a/AgentGuidelines/Tests/test_validate_guidelines.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Tests for the guideline repository validator.""" - -from __future__ import annotations - -import importlib.util -import json -import tempfile -import unittest -from pathlib import Path -from unittest import mock - - -VALIDATOR_PATH = Path(__file__).resolve().parents[1] / "Scripts" / "validate_guidelines.py" -SPEC = importlib.util.spec_from_file_location("validate_guidelines", VALIDATOR_PATH) -assert SPEC is not None -assert SPEC.loader is not None -VALIDATOR = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(VALIDATOR) - - -class SemanticVersionTests(unittest.TestCase): - """Verifies the supported Semantic Versioning grammar.""" - - def test_valid_versions(self) -> None: - """Accepts core, prerelease, and build metadata forms.""" - versions = ( - "0.0.2", - "1.2.3-rc.1+build.5", - "1.0.0-alpha-beta", - "1.0.0+001", - ) - - for version in versions: - with self.subTest(version=version): - self.assertIsNotNone(VALIDATOR.SEMVER.fullmatch(version)) - - def test_invalid_versions(self) -> None: - """Rejects leading zeroes and incomplete identifiers.""" - versions = ( - "01.2.3", - "1.02.3", - "1.2.03", - "1.2.3-01", - "1.2.3-rc.01", - "1.2.3+", - "1.2.3-", - ) - - for version in versions: - with self.subTest(version=version): - self.assertIsNone(VALIDATOR.SEMVER.fullmatch(version)) - - -class SwiftFormattingConfigurationTests(unittest.TestCase): - """Verifies the shared Swift formatting contract.""" - - def test_swift_format_configuration(self) -> None: - """Accepts the exhaustive Xcode-aligned swift-format configuration.""" - errors: list[str] = [] - - VALIDATOR.validate_swift_format_configuration(errors) - - self.assertEqual(errors, []) - - def test_swift_format_configuration_rejects_undocumented_rule_change(self) -> None: - """Rejects a changed rule value even when the exhaustive key set is unchanged.""" - configuration = json.loads( - VALIDATOR.SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8") - ) - configuration["rules"]["NeverForceUnwrap"] = True - - with tempfile.TemporaryDirectory(dir=VALIDATOR.ROOT) as directory: - path = Path(directory) / ".swift-format" - path.write_text(json.dumps(configuration), encoding="utf-8") - errors: list[str] = [] - - with mock.patch.object(VALIDATOR, "SWIFT_FORMAT_CONFIGURATION", path): - VALIDATOR.validate_swift_format_configuration(errors) - - self.assertTrue( - any("NeverForceUnwrap must be False, found True" in error for error in errors) - ) - - def test_swift_format_configuration_requires_conditional_import_sorting(self) -> None: - """Rejects disabling conditional import sorting.""" - configuration = json.loads( - VALIDATOR.SWIFT_FORMAT_CONFIGURATION.read_text(encoding="utf-8") - ) - configuration["orderedImports"]["includeConditionalImports"] = False - - with tempfile.TemporaryDirectory(dir=VALIDATOR.ROOT) as directory: - path = Path(directory) / ".swift-format" - path.write_text(json.dumps(configuration), encoding="utf-8") - errors: list[str] = [] - - with mock.patch.object(VALIDATOR, "SWIFT_FORMAT_CONFIGURATION", path): - VALIDATOR.validate_swift_format_configuration(errors) - - self.assertTrue( - any( - "orderedImports.includeConditionalImports must be True" in error - for error in errors - ) - ) - - def test_editor_configuration(self) -> None: - """Accepts the shared Swift EditorConfig values.""" - errors: list[str] = [] - - VALIDATOR.validate_editor_configuration(errors) - - self.assertEqual(errors, []) - - def test_swift_format_guideline(self) -> None: - """Accepts the documented package and non-mutating CI workflows.""" - errors: list[str] = [] - - VALIDATOR.validate_swift_format_guideline(errors) - - self.assertEqual(errors, []) - - -class AgentGuidelinesAuditSkillTests(unittest.TestCase): - """Verifies the mandatory completion-audit skill contract.""" - - def test_audit_skill_contract(self) -> None: - """Accepts the skill, Development rule, and consumer template.""" - errors: list[str] = [] - - VALIDATOR.validate_audit_skill(errors) - - self.assertEqual(errors, []) - - def test_consumer_setup_script(self) -> None: - """Accepts the executable deterministic consumer validator.""" - errors: list[str] = [] - - VALIDATOR.validate_consumer_setup_script(errors) - - self.assertEqual(errors, []) - - -if __name__ == "__main__": - unittest.main() diff --git a/AgentGuidelines/VERSION b/AgentGuidelines/VERSION index 32786aa..cd9d21e 100644 --- a/AgentGuidelines/VERSION +++ b/AgentGuidelines/VERSION @@ -1 +1 @@ -0.0.18 +0.0.33 From f9a9bbf94c39d4d4f76ec51619a380c21ce4444c Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Sat, 19 Sep 2026 16:20:18 +0200 Subject: [PATCH 2/2] Adopt strict Swift package settings --- .github/workflows/ci-pr.yml | 3 ++ .github/workflows/ci.yml | 3 ++ .gitignore | 67 +++++++++++++++++++++++++++++++++---- AGENTS.md | 50 +++++++++++++++++++++++++++ CHANGELOG.md | 9 +++++ Package.swift | 15 +++++++++ README.md | 2 +- VERSION | 1 + 8 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 VERSION diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 7ed9b55..812d031 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -17,6 +17,9 @@ jobs: - name: Checkout Repository uses: actions/checkout@v7 + - name: Validate Consumer Integration + run: AgentGuidelines/Scripts/validate_consumer_setup.swift + - name: Lint Swift Sources run: | AgentGuidelines/Scripts/swift_format.sh lint-strict \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b68c6d..cea53a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,9 @@ jobs: - name: Checkout Repository uses: actions/checkout@v7 + - name: Validate Consumer Integration + run: AgentGuidelines/Scripts/validate_consumer_setup.swift + - name: Lint Swift Sources run: | AgentGuidelines/Scripts/swift_format.sh lint-strict \ diff --git a/.gitignore b/.gitignore index 08c2ed7..3a3fc11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,66 @@ +# macOS .DS_Store -/.build -/Packages -/*.xcodeproj -*.xcworkspace + +# Xcode user data, build output, and archives xcuserdata/ -Package.resolved -DerivedData/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.moved-aside +*.xccheckout +*.xcscmblueprint +*.hmap +/build/ +/DerivedData/ +*.ipa +*.dSYM +*.dSYM.zip + +# Xcode playground generated state +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +/.build/ +/Packages/ .swiftpm/configuration/registries.json + +# Node.js +node_modules/ +dist/ +coverage/ +*.tgz +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Local credentials and environment configuration +.env +.env.* +!.env.example +!.env.*.example .netrc + +# ThatFactory tooling state +.appstore-connect-mcp/ +.devspace/ /public-check/ + +# Project-specific entries require a durable rationale after repository-owner review. +# Project-specific: diff --git a/AGENTS.md b/AGENTS.md index 2516863..268ceac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,3 +67,53 @@ Automatic Codex review is the initial review. Do not request a manual Codex revi ## 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. + + +## Runtime Observability + +Treat privacy-safe runtime observability as part of implementing or changing stateful, asynchronous, fallible, or lifecycle-oriented behavior. Identify the meaningful success, failure, cancellation, recovery, and state-transition boundaries before handoff, and emit concise AppLogger events owned by the artifact that implements them. Dependency declaration or target linkage alone does not satisfy this requirement. + +Every ThatFactory package log starts with its canonical emoji and uses its own stable subsystem. Never log credentials, account or record identifiers, share URLs, captured content, images, or other user-generated values as public metadata. Keep pure values and utilities silent when they have no meaningful diagnostic event; record that deliberate decision in the implementation handoff instead of adding initializer or property-access noise. Follow [Logging](AgentGuidelines/Guidelines/Logging.md) for ownership, privacy, severity, message design, and tests. + + + +## 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). + diff --git a/CHANGELOG.md b/CHANGELOG.md index fa818ca..3b91043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to ProgressionKit are documented here. +## 0.1.7 — 2026-09-19 + +### Changed + +- Adopted Agent Guidelines `0.0.33` and the Swift package compiler-settings baseline. +- Declared Swift 6, warnings as errors, and the required upcoming language features for every package target. +- Made imports and existential types explicit where required by the stricter compiler policy without intentionally changing runtime behavior. + + ## [0.1.6] - 2026-08-18 ### Changed diff --git a/Package.swift b/Package.swift index cf26039..be9d661 100644 --- a/Package.swift +++ b/Package.swift @@ -2,6 +2,15 @@ import PackageDescription +let strictSwiftSettings: [SwiftSetting] = [ + .treatAllWarnings(as: .error), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), +] + let package = Package( name: "ProgressionKit", platforms: [ @@ -42,3 +51,9 @@ let package = Package( ), ] ) + +package.swiftLanguageModes = [.v6] + +for target in package.targets { + target.swiftSettings = strictSwiftSettings +} diff --git a/README.md b/README.md index 8978b14..eff4bed 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ In your `Package.swift`, add `ProgressionKit` as a dependency: dependencies: [ .package( url: "https://github.com/thatfactory/progressionkit", - from: "0.1.6" + from: "0.1.7" ) ] ``` diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..1180819 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.7