ci: split workflows for truthful badges and attribution gates
Replace monolithic ci.yml with per-platform workflows so each README badge tracks one job. Scan commit messages for tool-agnostic AI attribution in CI, sync local hooks to the canon, and refresh README status plus git-discipline Rule 8.
This commit is contained in:
parent
93fd1f7815
commit
d2a9ba576b
@ -4,6 +4,10 @@
|
|||||||
# Enforces github-discipline.md Rule 5.1 (no AI attribution) and
|
# Enforces github-discipline.md Rule 5.1 (no AI attribution) and
|
||||||
# Rule 5.2 (no em dashes U+2014) at the commit boundary.
|
# Rule 5.2 (no em dashes U+2014) at the commit boundary.
|
||||||
#
|
#
|
||||||
|
# Tool-agnostic: synced from the canonical
|
||||||
|
# mihaela-agents/templates/git-hooks/commit-msg-style-check, plus an author /
|
||||||
|
# committer identity guard for AI-agent commit identities.
|
||||||
|
#
|
||||||
# Install (per repo): git config core.hooksPath .githooks
|
# Install (per repo): git config core.hooksPath .githooks
|
||||||
#
|
#
|
||||||
# Portable: uses only BSD-compatible grep (no -P, no \x escapes in patterns).
|
# Portable: uses only BSD-compatible grep (no -P, no \x escapes in patterns).
|
||||||
@ -35,16 +39,22 @@ if LC_ALL=C printf '%s' "$CONTENT" | grep -qF -- "$EMDASH"; then
|
|||||||
FAIL=1
|
FAIL=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rule 5.1: literal AI-attribution phrases.
|
# Rule 5.1: AI-attribution phrases. Tool-agnostic: any assistant or its vendor,
|
||||||
LITERAL_PATTERNS=(
|
# not just the one in use. Bare tool names are NOT flagged (that would
|
||||||
'Co-Authored-By: Claude'
|
# false-positive on legitimate text like "fix cursor blink"); only
|
||||||
'Co-authored-by: Claude'
|
# attribution-context mentions are. Case-insensitive, BSD-grep -E compatible.
|
||||||
'Generated with Claude Code'
|
AI_TOOLS='Claude|Anthropic|Codex|OpenAI|ChatGPT|GPT-[0-9]|Cursor|Copilot|Gemini|Google AI'
|
||||||
'Generated with Claude'
|
ATTRIB_REGEX="(Co-Authored-By|Co-authored-with|Generated (with|by)|Created (with|by)|Powered by|with help from|written by|authored by)[: ].*(${AI_TOOLS})"
|
||||||
'Generated by Claude'
|
if printf '%s' "$CONTENT" | grep -qiE -- "$ATTRIB_REGEX"; then
|
||||||
'with help from Claude'
|
printf 'commit-msg: forbidden AI-attribution phrase (names an AI tool/vendor).\n' >&2
|
||||||
'Powered by Claude'
|
printf ' Rule: github-discipline.md Rule 5.1 (tool-agnostic)\n' >&2
|
||||||
'powered by Claude'
|
printf ' Matched lines:\n' >&2
|
||||||
|
printf '%s\n' "$CONTENT" | grep -niE -- "$ATTRIB_REGEX" | sed 's/^/ /' >&2
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generic self-reference tells, independent of any vendor name.
|
||||||
|
GENERIC_PATTERNS=(
|
||||||
'as an AI'
|
'as an AI'
|
||||||
'As an AI'
|
'As an AI'
|
||||||
'this commit was generated'
|
'this commit was generated'
|
||||||
@ -52,7 +62,7 @@ LITERAL_PATTERNS=(
|
|||||||
'this change was generated'
|
'this change was generated'
|
||||||
'This change was generated'
|
'This change was generated'
|
||||||
)
|
)
|
||||||
for p in "${LITERAL_PATTERNS[@]}"; do
|
for p in "${GENERIC_PATTERNS[@]}"; do
|
||||||
if printf '%s' "$CONTENT" | grep -qF -- "$p"; then
|
if printf '%s' "$CONTENT" | grep -qF -- "$p"; then
|
||||||
printf 'commit-msg: forbidden AI-attribution phrase: %s\n' "$p" >&2
|
printf 'commit-msg: forbidden AI-attribution phrase: %s\n' "$p" >&2
|
||||||
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
||||||
@ -88,48 +98,15 @@ for p in "${PREAMBLE_PATTERNS[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Rule 5.1: Cursor attribution. Reject Cursor co-author trailers, the agent
|
# Author / committer identity must not be an AI agent. git var resolves the exact
|
||||||
# email, any @cursor.com address, and "generated with Cursor" style tells.
|
|
||||||
# Case-insensitive on purpose: cursoragent@cursor.com, Co-authored-by: Cursor.
|
|
||||||
CURSOR_LITERALS=(
|
|
||||||
'Co-Authored-By: Cursor'
|
|
||||||
'Co-authored-by: Cursor'
|
|
||||||
'Generated with Cursor'
|
|
||||||
'Generated by Cursor'
|
|
||||||
'with help from Cursor'
|
|
||||||
'Powered by Cursor'
|
|
||||||
)
|
|
||||||
for p in "${CURSOR_LITERALS[@]}"; do
|
|
||||||
if printf '%s' "$CONTENT" | grep -qiF -- "$p"; then
|
|
||||||
printf 'commit-msg: forbidden Cursor attribution: %s\n' "$p" >&2
|
|
||||||
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if printf '%s' "$CONTENT" | grep -qiE -- '@cursor\.com'; then
|
|
||||||
printf 'commit-msg: forbidden Cursor email (@cursor.com) in commit message.\n' >&2
|
|
||||||
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rule 5.1 is vendor-neutral: reject attribution for every AI assistant, not just
|
|
||||||
# Claude/Cursor. Vendor names live in a list so the hook stays tool-agnostic.
|
|
||||||
AI_VENDORS="Codex Copilot Gemini ChatGPT OpenAI Anthropic"
|
|
||||||
AI_PREFIXES='Co-Authored-By:|Co-authored-by:|Generated with|Generated by|with help from|Powered by'
|
|
||||||
for vendor in $AI_VENDORS; do
|
|
||||||
if printf '%s' "$CONTENT" | grep -qiE -- "(${AI_PREFIXES}) ${vendor}"; then
|
|
||||||
printf 'commit-msg: forbidden AI attribution for %s.\n' "$vendor" >&2
|
|
||||||
printf ' Rule: github-discipline.md Rule 5.1 (tool-agnostic)\n' >&2
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Author / committer identity must not be Cursor. git var resolves the exact
|
|
||||||
# identity (including --author / GIT_*_IDENT overrides) this commit will record.
|
# identity (including --author / GIT_*_IDENT overrides) this commit will record.
|
||||||
|
# Conservative on names (a human may be named Claude or Gemini); matches agent
|
||||||
|
# email domains and the Cursor agent identity, which injects itself in practice.
|
||||||
|
IDENT_REGEX='@cursor\.com|cursoragent|(^|[^[:alpha:]])[Cc]ursor [Aa]gent([^[:alpha:]]|$)'
|
||||||
for ident in "$(git var GIT_AUTHOR_IDENT 2>/dev/null)" "$(git var GIT_COMMITTER_IDENT 2>/dev/null)"; do
|
for ident in "$(git var GIT_AUTHOR_IDENT 2>/dev/null)" "$(git var GIT_COMMITTER_IDENT 2>/dev/null)"; do
|
||||||
[ -z "$ident" ] && continue
|
[ -z "$ident" ] && continue
|
||||||
if printf '%s' "$ident" | grep -qiE -- '@cursor\.com|(^|[^[:alpha:]])cursor(agent)?([^[:alpha:]]|$)'; then
|
if printf '%s' "$ident" | grep -qiE -- "$IDENT_REGEX"; then
|
||||||
printf 'commit-msg: forbidden Cursor identity as author/committer: %s\n' "$ident" >&2
|
printf 'commit-msg: forbidden AI-agent identity as author/committer: %s\n' "$ident" >&2
|
||||||
printf ' Fix: set git config user.name / user.email to your own identity.\n' >&2
|
printf ' Fix: set git config user.name / user.email to your own identity.\n' >&2
|
||||||
FAIL=1
|
FAIL=1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -75,10 +75,8 @@ SKIP_PATTERNS=(
|
|||||||
'/pre-commit$'
|
'/pre-commit$'
|
||||||
'/pre-push$'
|
'/pre-push$'
|
||||||
'/check-style\.sh$'
|
'/check-style\.sh$'
|
||||||
|
'/check-commit-attribution\.sh$'
|
||||||
'/check-staged-style\.sh$'
|
'/check-staged-style\.sh$'
|
||||||
# The CI workflow that enforces the same Cursor prohibition server-side; it
|
|
||||||
# must quote the forbidden tokens to scan for them.
|
|
||||||
'^\.github/workflows/no-cursor-attribution\.yml$'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter STAGED_FILES through the skip list.
|
# Filter STAGED_FILES through the skip list.
|
||||||
|
|||||||
89
.github/workflows/ci.yml
vendored
89
.github/workflows/ci.yml
vendored
@ -1,89 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
# PureXML is a root Swift package. CI must prove macOS, Linux, Windows, and WASM.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 6 * * 1"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
macos:
|
|
||||||
name: macOS
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Install format and lint tools
|
|
||||||
run: brew install swiftformat swiftlint
|
|
||||||
- name: Repository gates
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
bash scripts/check-style.sh
|
|
||||||
bash scripts/check-namespacing.sh
|
|
||||||
bash scripts/check-forbidden-patterns.sh
|
|
||||||
- name: Format and lint
|
|
||||||
run: |
|
|
||||||
swiftformat . --config .swiftformat --lint
|
|
||||||
swiftlint --config .swiftlint.yml --strict
|
|
||||||
- name: Build and test
|
|
||||||
run: |
|
|
||||||
swift build
|
|
||||||
swift test
|
|
||||||
swift build -c release
|
|
||||||
swift test -c release
|
|
||||||
|
|
||||||
linux:
|
|
||||||
name: Linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: swift:6.2
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Build and test
|
|
||||||
run: bash scripts/check-linux.sh
|
|
||||||
|
|
||||||
windows:
|
|
||||||
name: Windows
|
|
||||||
runs-on: windows-2022
|
|
||||||
timeout-minutes: 45
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Install Swift
|
|
||||||
uses: compnerd/gha-setup-swift@main
|
|
||||||
with:
|
|
||||||
swift-version: swift-6.3.2-release
|
|
||||||
swift-build: 6.3.2-RELEASE
|
|
||||||
- name: Build and test
|
|
||||||
shell: pwsh
|
|
||||||
run: ./scripts/check-windows.ps1
|
|
||||||
|
|
||||||
wasm:
|
|
||||||
name: WASM
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Install Swift toolchain dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libcurl4-openssl-dev
|
|
||||||
- name: Install swiftly
|
|
||||||
run: |
|
|
||||||
curl -L https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz > swiftly.tar.gz
|
|
||||||
tar zxf swiftly.tar.gz
|
|
||||||
./swiftly init --quiet-shell-followup --assume-yes --skip-install
|
|
||||||
- name: Install Swift 6.3.2 and Wasm SDK
|
|
||||||
run: |
|
|
||||||
. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"
|
|
||||||
swiftly install --use 6.3.2
|
|
||||||
swift sdk install https://download.swift.org/swift-6.3.2-release/wasm-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_wasm.artifactbundle.tar.gz --checksum a61f0584c93283589f8b2f42db05c1f9a182b506c2957271402992655591dd7c
|
|
||||||
- name: Build for WASM
|
|
||||||
run: |
|
|
||||||
. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"
|
|
||||||
bash scripts/check-wasm.sh
|
|
||||||
60
.github/workflows/no-cursor-attribution.yml
vendored
60
.github/workflows/no-cursor-attribution.yml
vendored
@ -1,60 +0,0 @@
|
|||||||
name: No Cursor attribution
|
|
||||||
|
|
||||||
# Server-side companion to the local .githooks/commit-msg and pre-commit guards.
|
|
||||||
# Fails the check if any commit pushed (or in a PR) carries Cursor attribution:
|
|
||||||
# a Cursor co-author trailer, an @cursor.com email, or a "generated with Cursor"
|
|
||||||
# tell, in the commit message or in the author/committer identity.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
scan:
|
|
||||||
name: Scan commits
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Reject Cursor attribution
|
|
||||||
env:
|
|
||||||
EVENT: ${{ github.event_name }}
|
|
||||||
PR_BASE: ${{ github.event.pull_request.base.sha }}
|
|
||||||
PR_HEAD: ${{ github.event.pull_request.head.sha }}
|
|
||||||
PUSH_BEFORE: ${{ github.event.before }}
|
|
||||||
SHA: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Resolve the set of commits to scan.
|
|
||||||
if [ "$EVENT" = "pull_request" ]; then
|
|
||||||
range="${PR_BASE}..${PR_HEAD}"
|
|
||||||
elif [ -n "${PUSH_BEFORE:-}" ] && [ "$PUSH_BEFORE" != "0000000000000000000000000000000000000000" ]; then
|
|
||||||
range="${PUSH_BEFORE}..${SHA}"
|
|
||||||
else
|
|
||||||
# New branch / tag push or manual run: scan just the tip commit.
|
|
||||||
range="${SHA}~1..${SHA}"
|
|
||||||
git rev-parse "${SHA}~1" >/dev/null 2>&1 || range="$SHA"
|
|
||||||
fi
|
|
||||||
echo "Scanning commit range: $range"
|
|
||||||
|
|
||||||
# Emit message + author + committer for each commit, then grep.
|
|
||||||
fmt='commit %H%nauthor: %an <%ae>%ncommitter: %cn <%ce>%n%B'
|
|
||||||
if printf '%s' "$range" | grep -q '\.\.'; then
|
|
||||||
log=$(git log --format="$fmt" "$range")
|
|
||||||
else
|
|
||||||
log=$(git log -1 --format="$fmt" "$range")
|
|
||||||
fi
|
|
||||||
|
|
||||||
pattern='cursoragent@cursor\.com|@cursor\.com|Co-authored-by:[[:space:]]*Cursor|Generated (with|by) Cursor|with help from Cursor|Powered by Cursor'
|
|
||||||
if printf '%s\n' "$log" | grep -niE -- "$pattern"; then
|
|
||||||
echo "::error::Cursor attribution found in the commits above. Strip it (rewrite the commits) before pushing."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "No Cursor attribution found."
|
|
||||||
34
.github/workflows/style.yml
vendored
Normal file
34
.github/workflows/style.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Style and namespacing
|
||||||
|
|
||||||
|
# One workflow per concern so each README badge tracks exactly one job and is
|
||||||
|
# truthful. This job runs the repository gates (style, commit-message
|
||||||
|
# attribution, namespacing, forbidden patterns).
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
style:
|
||||||
|
name: Style and namespacing
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Repository gates
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: |
|
||||||
|
bash scripts/check-style.sh
|
||||||
|
bash scripts/check-commit-attribution.sh
|
||||||
|
bash scripts/check-namespacing.sh
|
||||||
|
bash scripts/check-forbidden-patterns.sh
|
||||||
20
.github/workflows/swift-linux.yml
vendored
Normal file
20
.github/workflows/swift-linux.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
name: Swift (Linux)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
swift-linux:
|
||||||
|
name: Swift (Linux)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: swift:6.2
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Build and test
|
||||||
|
run: bash scripts/check-linux.sh
|
||||||
29
.github/workflows/swift-macos.yml
vendored
Normal file
29
.github/workflows/swift-macos.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Swift (macOS)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
swift-macos:
|
||||||
|
name: Swift (macOS)
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Install format and lint tools
|
||||||
|
run: brew install swiftformat swiftlint
|
||||||
|
- name: Format and lint
|
||||||
|
run: |
|
||||||
|
swiftformat . --config .swiftformat --lint
|
||||||
|
swiftlint --config .swiftlint.yml --strict
|
||||||
|
- name: Build and test
|
||||||
|
run: |
|
||||||
|
swift build
|
||||||
|
swift test
|
||||||
|
swift build -c release
|
||||||
|
swift test -c release
|
||||||
35
.github/workflows/swift-wasm.yml
vendored
Normal file
35
.github/workflows/swift-wasm.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Swift (WASM)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
swift-wasm:
|
||||||
|
name: Swift (WASM)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Install Swift toolchain dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libcurl4-openssl-dev
|
||||||
|
- name: Install swiftly
|
||||||
|
run: |
|
||||||
|
curl -L https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz > swiftly.tar.gz
|
||||||
|
tar zxf swiftly.tar.gz
|
||||||
|
./swiftly init --quiet-shell-followup --assume-yes --skip-install
|
||||||
|
- name: Install Swift 6.3.2 and Wasm SDK
|
||||||
|
run: |
|
||||||
|
. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"
|
||||||
|
swiftly install --use 6.3.2
|
||||||
|
swift sdk install https://download.swift.org/swift-6.3.2-release/wasm-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_wasm.artifactbundle.tar.gz --checksum a61f0584c93283589f8b2f42db05c1f9a182b506c2957271402992655591dd7c
|
||||||
|
- name: Build for WASM
|
||||||
|
run: |
|
||||||
|
. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"
|
||||||
|
bash scripts/check-wasm.sh
|
||||||
26
.github/workflows/swift-windows.yml
vendored
Normal file
26
.github/workflows/swift-windows.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name: Swift (Windows)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
swift-windows:
|
||||||
|
name: Swift (Windows)
|
||||||
|
runs-on: windows-2022
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Install Swift
|
||||||
|
uses: compnerd/gha-setup-swift@main
|
||||||
|
with:
|
||||||
|
swift-version: swift-6.3.2-release
|
||||||
|
swift-build: 6.3.2-RELEASE
|
||||||
|
- name: Build and test
|
||||||
|
shell: pwsh
|
||||||
|
run: ./scripts/check-windows.ps1
|
||||||
98
README.md
98
README.md
@ -1,8 +1,11 @@
|
|||||||
# PureXML
|
# PureXML
|
||||||
|
|
||||||
[](https://github.com/mihaelamj/PureXML/actions/workflows/ci.yml)
|
[](https://github.com/mihaelamj/PureXML/actions/workflows/style.yml)
|
||||||
[](https://github.com/mihaelamj/PureXML/actions/workflows/ci.yml)
|
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-macos.yml)
|
||||||
[](https://github.com/mihaelamj/PureXML/actions/workflows/ci.yml)
|
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-linux.yml)
|
||||||
|
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-windows.yml)
|
||||||
|
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-wasm.yml)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
PureXML is a dependency-free XML package written entirely in Swift.
|
PureXML is a dependency-free XML package written entirely in Swift.
|
||||||
|
|
||||||
@ -126,27 +129,66 @@ flowchart TB
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
The node model, the emitter, and a streaming parser are implemented and usable
|
PureXML is a working, dependency-free XML library today: parse, emit, validate,
|
||||||
today.
|
query, and stream documents on macOS, Linux, Windows, and WASM. The test suite
|
||||||
|
currently runs **135 tests in 18 suites** (`swift test`).
|
||||||
|
|
||||||
- **Model** (`PureXML.Model`): `Node`, `Element`, `Attribute`, `QualifiedName`.
|
### Shipped (libxml2-aligned surface)
|
||||||
Preserves document order and the distinction between text, CDATA, comments,
|
|
||||||
and processing instructions.
|
- **Model** (`PureXML.Model`): `Node`, `Element`, `Attribute`, `QualifiedName`,
|
||||||
- **Emitting** (`PureXML.Emitting`): `Serializer` turns a node tree into
|
and a mutable `TreeNode` for in-place editing (insert, remove, replace, copy,
|
||||||
well-formed XML, with pretty-printed and compact options and correct text and
|
parent navigation).
|
||||||
attribute escaping.
|
- **Parsing** (`PureXML.Parsing`): iterative streaming parser over strings,
|
||||||
- **Validation** (`PureXML.Validation`): structural checks such as duplicate
|
bytes, or incremental character/byte sources. Pull events via `EventReader`,
|
||||||
attribute names. Schema validation (DTD/XSD/RELAX NG) is out of scope for the
|
SAX2-style callbacks via `SAXHandler`, and a pull cursor via `TextReader`
|
||||||
library target.
|
(libxml2 `xmlTextReader`). Handles elements, attributes, text, predefined and
|
||||||
- **Parsing** (`PureXML.Parsing`): a streaming, iterative parser. `EventReader`
|
numeric entities, comments, CDATA, and processing instructions. Namespace
|
||||||
is a pull-based event core that consumes input through a character-source
|
bindings (`xmlns`) are resolved to URIs on parse.
|
||||||
closure and emits one `Event` at a time, never holding the whole document in
|
- **Encoding**: UTF-8/16/32 (with BOM sniffing), ISO-8859-1, and Windows-1252
|
||||||
memory (it drives chunked input). `Parser` builds a `Model.Node` tree
|
from raw bytes or streaming byte input.
|
||||||
iteratively over the event core. Handles elements, attributes, text, the five
|
- **DTD (opt-in)**: `<!DOCTYPE>` is **off by default** (`Limits.allowDoctype:
|
||||||
predefined entities and numeric character references, comments, CDATA, and
|
false`) to keep XXE and entity-expansion closed. When enabled, the internal
|
||||||
processing instructions. Safe by default: `<!DOCTYPE>` is rejected, which
|
subset is parsed; internal general entities expand with a bounded cap; external
|
||||||
removes the DTD-based threat classes (XXE, entity-expansion DoS). DTD support,
|
entities and external subsets are refused unless an injected
|
||||||
namespaces-as-resolved-URIs, and validation are deliberate future layers.
|
`EntityResolver` supplies replacement text.
|
||||||
|
- **Validation** (`PureXML.Validation`): structural checks (duplicate attributes,
|
||||||
|
etc.) plus **DTD validation** against declared content models, attribute lists,
|
||||||
|
and ID/IDREF rules (`DTDSchema`, `validateAgainstInternalDTD`). XSD, RELAX NG,
|
||||||
|
and Schematron remain open epics (#2, #25).
|
||||||
|
- **Emitting** (`PureXML.Emitting`): `Serializer` (save-option parity) and an
|
||||||
|
incremental `Writer` (libxml2 `xmlTextWriter`) with compact and pretty modes.
|
||||||
|
- **XPath** (`PureXML.XPath`): a **subset** of XPath 1.0 (location paths, common
|
||||||
|
axes, node tests, basic predicates). Full XPath 1.0 is epic #21.
|
||||||
|
|
||||||
|
### Safe defaults
|
||||||
|
|
||||||
|
By default the parser rejects `<!DOCTYPE>` and refuses every external reference
|
||||||
|
(`EntityResolver.refusing`). Enable DTD processing only when you need internal
|
||||||
|
subset validation or internal entities, and wire a resolver only for trusted,
|
||||||
|
in-memory replacements. Network fetching (`nanohttp`/`nanoftp`) and libxml2's
|
||||||
|
threading/memory infrastructure are deliberate non-goals.
|
||||||
|
|
||||||
|
### Remaining toward full libxml2 feature parity
|
||||||
|
|
||||||
|
**12 of 20 epics are still open**; each now has filed child issues (#4–#60). The
|
||||||
|
shipped eight cover the core I/O stack; most remaining work is whole standards:
|
||||||
|
|
||||||
|
| Open epic | Child issues | What it adds |
|
||||||
|
|---|---|---|
|
||||||
|
| #1 Push / feed API | #4–#7 | Resumable scanner + push parser |
|
||||||
|
| #20 HTML parser | #36–#39 | Tag-soup HTML parse and serialize |
|
||||||
|
| #21 Full XPath 1.0 | #40–#43 | Unlocks #22, #23, #25, #3 |
|
||||||
|
| #22 xmlPattern | #44–#45 | Streaming pattern match (needs #21) |
|
||||||
|
| #23 XPointer | #46–#47 | element() / xpointer() (needs #21) |
|
||||||
|
| #24 XInclude | #48–#51 | URI, xml:base, xi:include |
|
||||||
|
| #25 Schematron | #52–#53 | Rule validation (needs #21) |
|
||||||
|
| #26 C14N | #54–#56 | Canonical XML variants |
|
||||||
|
| #27 XML Catalog | #57–#58 | OASIS catalog resolver |
|
||||||
|
| #30 → #2 | #59–#60, #8–#13 | Regex engine, then XSD / RELAX NG |
|
||||||
|
| #3 XSLT 1.0 | #14–#18 | Stylesheet transform (needs #21) |
|
||||||
|
|
||||||
|
**40 open enhancement issues** under those epics (plus the 12 epic parents). Critical
|
||||||
|
path: **#30 → #2** (schema) and **#21 → #22/#23/#25/#3** (query/transform).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -171,6 +213,16 @@ var reader = PureXML.events(xml)
|
|||||||
while let event = try reader.next() {
|
while let event = try reader.next() {
|
||||||
// handle .startElement / .characters / .endElement / ...
|
// handle .startElement / .characters / .endElement / ...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DTD validation (opt-in: allowDoctype + optional resolver for trusted externals).
|
||||||
|
let issues = try PureXML.validateAgainstInternalDTD(
|
||||||
|
xmlWithDoctype,
|
||||||
|
limits: .init(allowDoctype: true),
|
||||||
|
)
|
||||||
|
|
||||||
|
// XPath subset over a parsed tree.
|
||||||
|
let titles = try PureXML.xpath("//title", over: node)
|
||||||
|
.compactMap(\.element)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
public extension PureXML {
|
public extension PureXML {
|
||||||
/// Parses an XML document into a ``Model/Node`` tree.
|
/// Parses an XML document into a ``Model/Node`` tree.
|
||||||
///
|
|
||||||
/// The parser is still being built; this entry point currently raises
|
|
||||||
/// ``Parsing/ParseError/notImplemented(_:)``. The serializer and model are
|
|
||||||
/// usable today for building and emitting trees.
|
|
||||||
static func parse(
|
static func parse(
|
||||||
_ xml: String,
|
_ xml: String,
|
||||||
limits: Parsing.Limits = .default,
|
limits: Parsing.Limits = .default,
|
||||||
|
|||||||
@ -238,6 +238,54 @@ A repo that adopts these rules should ship a mechanical backstop:
|
|||||||
|
|
||||||
**Why mechanical enforcement matters.** A disciplined author following Rule 5.2 can still push a commit whose message contains an em dash, because nothing at the commit boundary stops it. A 3-line `commit-msg` hook prevents this entire failure class at write time, after which `git push` cannot carry a violation regardless of who or what wrote the message. Discipline scales with attention; hooks do not.
|
**Why mechanical enforcement matters.** A disciplined author following Rule 5.2 can still push a commit whose message contains an em dash, because nothing at the commit boundary stops it. A 3-line `commit-msg` hook prevents this entire failure class at write time, after which `git push` cannot carry a violation regardless of who or what wrote the message. Discipline scales with attention; hooks do not.
|
||||||
|
|
||||||
|
## 8. CI workflows and truthful badges
|
||||||
|
|
||||||
|
### Rule 8.1: One workflow file per concern, one badge per workflow
|
||||||
|
|
||||||
|
Each CI concern (style/gates, and each build-or-test platform) is its own workflow file with a unique `name:`. A README badge points at that one workflow's status endpoint, so the badge tracks exactly one job and is truthful.
|
||||||
|
|
||||||
|
Do NOT use a single multi-job workflow (`ci.yml`) with several shields.io `?label=...` badges that all point at the same workflow. The `actions/workflow/status` endpoint reports the whole-workflow status; the `label=` is cosmetic, so every such badge renders the same result. A failure in any one job turns all of them red, and none of them tracks its named platform. That is a lying badge.
|
||||||
|
|
||||||
|
Canonical layout (one file each, names are the badge labels):
|
||||||
|
|
||||||
|
```
|
||||||
|
.github/workflows/style.yml # name: Style and namespacing
|
||||||
|
.github/workflows/swift-macos.yml # name: Swift (macOS)
|
||||||
|
.github/workflows/swift-linux.yml # name: Swift (Linux)
|
||||||
|
.github/workflows/swift-windows.yml # name: Swift (Windows)
|
||||||
|
.github/workflows/swift-wasm.yml # name: Swift (WASM)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rule 8.2: Native per-workflow badge markdown, plus a license badge
|
||||||
|
|
||||||
|
Use GitHub's native per-workflow badge (it reflects the real status of that one workflow), not a shields.io workflow-status badge with a cosmetic label:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[](https://github.com/<owner>/<repo>/actions/workflows/style.yml)
|
||||||
|
[](https://github.com/<owner>/<repo>/actions/workflows/swift-macos.yml)
|
||||||
|
[](https://github.com/<owner>/<repo>/actions/workflows/swift-linux.yml)
|
||||||
|
[](LICENSE)
|
||||||
|
```
|
||||||
|
|
||||||
|
The license badge is static and links to the repo's `LICENSE`; match the label to the actual license (`MIT`, `AGPL--3.0`, etc.).
|
||||||
|
|
||||||
|
### Rule 8.3: Add a platform job and badge ONLY when that platform is genuinely possible
|
||||||
|
|
||||||
|
A badge is a claim. Only claim a platform the repo can actually build or test on. Never ship a workflow or badge for a platform the package does not support; an absent badge is honest, a red-or-fictional badge is not.
|
||||||
|
|
||||||
|
Decide possibility from the repo's topology (see `cross-platform.md`):
|
||||||
|
|
||||||
|
- **Apple platforms (iOS, visionOS, tvOS, watchOS) and any UI build or test run on macOS runners only.** There is no "iOS CI on Linux" and no Windows/Linux UI. An iOS/visionOS app or a SwiftUI/UIKit/AppKit target is exercised by a macOS-hosted job (simulator or `xcodebuild -destination`), reported under the macOS badge or a dedicated `swift-ios.yml` that still `runs-on: macos`. Do not add a Linux or Windows badge for UI code.
|
||||||
|
- **Linux** badge only when the product builds on a Linux toolchain (libraries, servers, CLIs). A pure Apple-UI repo has no Linux badge.
|
||||||
|
- **Windows** badge only when the targets compile on Windows. If the test tooling is POSIX-only, Windows is a build-only gate: build the portable library targets (debug and release), skip the suite, and say so in a header comment. The badge then truthfully means "compiles on Windows."
|
||||||
|
- **WASM/WASI** badge only when the package builds for `wasm32-unknown-wasip1`. Code that needs Foundation networking, threads, or other WASI-absent surface does not get a WASM badge. WASM is typically a build gate; running wasm tests needs a separate runtime and is usually out of scope.
|
||||||
|
|
||||||
|
When a platform can build but not run the full suite, the workflow is a build gate, not a test gate. The header comment states the scope so the green badge is not read as "tests pass" when it means "compiles."
|
||||||
|
|
||||||
|
### Rule 8.4: Triggers and permissions are uniform across the split
|
||||||
|
|
||||||
|
Every workflow uses the same trigger surface so the badges refresh together: `on: push: branches: [main]` plus `pull_request:` plus `workflow_dispatch:`. Keep `permissions:` least-privilege (`contents: read`, plus `issues: read` only where a gate reads issues, e.g. a roadmap check). The commit-message attribution gate (Rule 5.1) lives in the style workflow and needs `fetch-depth: 0` on checkout plus `BASE_SHA` / `HEAD_SHA` from the PR event to scan the commit range.
|
||||||
|
|
||||||
## Triggers (when to load this rule)
|
## Triggers (when to load this rule)
|
||||||
|
|
||||||
Load this rule when:
|
Load this rule when:
|
||||||
@ -251,5 +299,6 @@ Load this rule when:
|
|||||||
- Running `git commit --amend` (Rule 5.3)
|
- Running `git commit --amend` (Rule 5.3)
|
||||||
- Running `git push` (Rule 6.1, Rule 6.2)
|
- Running `git push` (Rule 6.1, Rule 6.2)
|
||||||
- Designing a tracker-discipline mechanism for a new repo (Rule 7)
|
- Designing a tracker-discipline mechanism for a new repo (Rule 7)
|
||||||
|
- Setting up CI workflows or README status badges (Rule 8.1, Rule 8.2, Rule 8.3, Rule 8.4)
|
||||||
|
|
||||||
Load on demand per the trigger; do not auto-load.
|
Load on demand per the trigger; do not auto-load.
|
||||||
|
|||||||
108
scripts/check-commit-attribution.sh
Executable file
108
scripts/check-commit-attribution.sh
Executable file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Scan commit MESSAGES (and author/committer identity) in a range for
|
||||||
|
# AI-attribution tells. Tool-agnostic.
|
||||||
|
#
|
||||||
|
# Complements scripts/check-style.sh, which scans tracked FILE CONTENT via
|
||||||
|
# `git ls-files` and therefore never sees commit messages. An attribution
|
||||||
|
# trailer (e.g. "Co-authored-by: Cursor") lives in the message, not a file, so
|
||||||
|
# without this scan a poisoned trailer reaches the remote with no CI backstop.
|
||||||
|
#
|
||||||
|
# This is the merge-time backstop for github-discipline.md Rule 5.1; the local
|
||||||
|
# .githooks/commit-msg hook is the write-time backstop. Some assistants re-add a
|
||||||
|
# trailer on every commit/amend even when told not to, so neither the per-tool
|
||||||
|
# setting nor discipline alone is enough.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/check-commit-attribution.sh [<range>]
|
||||||
|
# Range resolution (first that applies):
|
||||||
|
# 1) explicit "<range>" argument (e.g. origin/main..HEAD)
|
||||||
|
# 2) BASE_SHA and HEAD_SHA environment variables -> BASE_SHA..HEAD_SHA
|
||||||
|
# 3) fallback: just HEAD (single commit)
|
||||||
|
#
|
||||||
|
# Portable: BSD-compatible grep, bash 3.2 (macOS) and bash 4+ (CI).
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
RANGE="${1:-}"
|
||||||
|
if [ -z "$RANGE" ]; then
|
||||||
|
if [ -n "${BASE_SHA:-}" ] && [ -n "${HEAD_SHA:-}" ]; then
|
||||||
|
RANGE="${BASE_SHA}..${HEAD_SHA}"
|
||||||
|
else
|
||||||
|
RANGE="HEAD~0..HEAD"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve the commit list. If the range is unusable (shallow clone, unknown
|
||||||
|
# base), fall back to HEAD so we always scan at least the tip.
|
||||||
|
COMMITS=$(git rev-list "$RANGE" 2>/dev/null || true)
|
||||||
|
[ -z "$COMMITS" ] && COMMITS=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
# Tool-agnostic AI-attribution detection (attribution-context only, so prose
|
||||||
|
# that merely contains a tool word is not a false positive). Mirrors
|
||||||
|
# .githooks/commit-msg and scripts/check-style.sh.
|
||||||
|
AI_TOOLS='Claude|Anthropic|Codex|OpenAI|ChatGPT|GPT-[0-9]|Cursor|Copilot|Gemini|Google AI'
|
||||||
|
ATTRIB_REGEX="^(Co-Authored-By|Co-authored-with|Generated (with|by)|Created (with|by)|Powered by|with help from|written by|authored by)[: ].*(${AI_TOOLS})"
|
||||||
|
|
||||||
|
GENERIC_PATTERNS=(
|
||||||
|
'as an AI'
|
||||||
|
'this commit was generated'
|
||||||
|
'this change was generated'
|
||||||
|
)
|
||||||
|
|
||||||
|
# AI-tell signature emojis, kept as bytes so this file carries no glyphs.
|
||||||
|
EMOJI_TELLS=$(printf '\xf0\x9f\xa4\x96\n\xe2\x9c\xa8\n\xf0\x9f\xaa\x84\n\xf0\x9f\xa7\xa0\n\xf0\x9f\xa6\xbe\n\xf0\x9f\xa4\x9d')
|
||||||
|
EMDASH=$(printf '\xe2\x80\x94')
|
||||||
|
|
||||||
|
# AI-agent author/committer identity. Conservative on names (a human may be
|
||||||
|
# named Claude or Gemini); matches agent email domains and the Cursor agent
|
||||||
|
# identity, which injects itself in practice.
|
||||||
|
IDENT_REGEX='@cursor\.com|cursoragent|(^|[^[:alpha:]])[Cc]ursor [Aa]gent([^[:alpha:]]|$)'
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
for sha in $COMMITS; do
|
||||||
|
MSG=$(git log -1 --format='%B' "$sha")
|
||||||
|
IDENT=$(git log -1 --format='%an <%ae>%n%cn <%ce>' "$sha")
|
||||||
|
short=$(git rev-parse --short "$sha")
|
||||||
|
|
||||||
|
# Attribution trailer/phrase, anchored to line start so a body sentence that
|
||||||
|
# describes the trailer (documentation) is not flagged, only a real trailer.
|
||||||
|
if printf '%s\n' "$MSG" | grep -qiE -- "$ATTRIB_REGEX"; then
|
||||||
|
printf 'commit-attribution: %s names an AI tool/vendor in attribution context.\n' "$short" >&2
|
||||||
|
printf '%s\n' "$MSG" | grep -niE -- "$ATTRIB_REGEX" | sed 's/^/ /' >&2
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for p in "${GENERIC_PATTERNS[@]}"; do
|
||||||
|
if printf '%s' "$MSG" | grep -qiF -- "$p"; then
|
||||||
|
printf 'commit-attribution: %s contains forbidden phrase: %s\n' "$short" "$p" >&2
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if LC_ALL=C printf '%s' "$MSG" | grep -qF -- "$EMDASH"; then
|
||||||
|
printf 'commit-attribution: %s contains an em dash (U+2014).\n' "$short" >&2
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r emoji; do
|
||||||
|
[ -z "$emoji" ] && continue
|
||||||
|
if LC_ALL=C printf '%s' "$MSG" | grep -qF -- "$emoji"; then
|
||||||
|
printf 'commit-attribution: %s contains an AI-signature emoji.\n' "$short" >&2
|
||||||
|
FAIL=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done <<<"$EMOJI_TELLS"
|
||||||
|
|
||||||
|
if printf '%s' "$IDENT" | grep -qiE -- "$IDENT_REGEX"; then
|
||||||
|
printf 'commit-attribution: %s has an AI-agent author/committer identity:\n' "$short" >&2
|
||||||
|
printf '%s\n' "$IDENT" | grep -niE -- "$IDENT_REGEX" | sed 's/^/ /' >&2
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$FAIL" -ne 0 ]; then
|
||||||
|
printf 'commit-attribution: gate failed. Rule: github-discipline.md Rule 5.1 / 5.2 (tool-agnostic).\n' >&2
|
||||||
|
printf ' Rebuild the offending commit with a clean message (git commit-tree bypasses\n' >&2
|
||||||
|
printf ' any assistant trailer injection), then re-push.\n' >&2
|
||||||
|
fi
|
||||||
|
exit "$FAIL"
|
||||||
@ -16,8 +16,7 @@ EMDASH=$(printf '\xe2\x80\x94')
|
|||||||
# detect them, so exclude them from the phrase scan.
|
# detect them, so exclude them from the phrase scan.
|
||||||
is_enforcement_file() {
|
is_enforcement_file() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
.githooks/*|scripts/check-style.sh) return 0 ;;
|
.githooks/*|scripts/check-style.sh|scripts/check-commit-attribution.sh) return 0 ;;
|
||||||
.github/workflows/no-cursor-attribution.yml) return 0 ;;
|
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user