Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project are documented in this file.

## [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
Expand Down
3 changes: 2 additions & 1 deletion Guidelines/Packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ From the consumer repository root, install a tagged release:
git subtree add \
--prefix=AgentGuidelines \
https://github.com/thatfactory/agent-guidelines.git \
0.0.29 \
0.0.30 \
--squash
```

Expand Down Expand Up @@ -147,7 +147,7 @@ Review the target release's changelog, then pull it deliberately:
git subtree pull \
--prefix=AgentGuidelines \
https://github.com/thatfactory/agent-guidelines.git \
0.0.29 \
0.0.30 \
--squash
```

Expand Down
74 changes: 74 additions & 0 deletions Scripts/validate_consumer_setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ let strictFormatCommandPattern =
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 {
Expand Down Expand Up @@ -167,6 +171,75 @@ func files(in directory: URL, extensions: Set<String>) -> [URL] {
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")
Expand Down Expand Up @@ -304,6 +377,7 @@ func validateConsumerSetup(
if swiftFormatAdopted {
validateSwiftFormatCI(consumerRoot: consumerRoot, errors: &errors)
}
validatePackageReadme(consumerRoot: consumerRoot, errors: &errors)
}

/// Parses command-line arguments.
Expand Down
4 changes: 4 additions & 0 deletions Scripts/validate_guidelines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@ func validateExternalDependencyPolicy(_ errors: inout [String]) {
"[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)'")
Expand Down
136 changes: 136 additions & 0 deletions Tests/run_tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,47 @@ func initializeRepository(at root: URL) throws {
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)"}}"#
}
Expand Down Expand Up @@ -469,6 +510,101 @@ let tests: [(String, () throws -> Void)] = [
}
}
),
(
"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",
{
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.29
0.0.30