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
|
||||
# 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
|
||||
#
|
||||
# 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
|
||||
fi
|
||||
|
||||
# Rule 5.1: literal AI-attribution phrases.
|
||||
LITERAL_PATTERNS=(
|
||||
'Co-Authored-By: Claude'
|
||||
'Co-authored-by: Claude'
|
||||
'Generated with Claude Code'
|
||||
'Generated with Claude'
|
||||
'Generated by Claude'
|
||||
'with help from Claude'
|
||||
'Powered by Claude'
|
||||
'powered by Claude'
|
||||
# Rule 5.1: AI-attribution phrases. Tool-agnostic: any assistant or its vendor,
|
||||
# not just the one in use. Bare tool names are NOT flagged (that would
|
||||
# false-positive on legitimate text like "fix cursor blink"); only
|
||||
# attribution-context mentions are. Case-insensitive, BSD-grep -E compatible.
|
||||
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})"
|
||||
if printf '%s' "$CONTENT" | grep -qiE -- "$ATTRIB_REGEX"; then
|
||||
printf 'commit-msg: forbidden AI-attribution phrase (names an AI tool/vendor).\n' >&2
|
||||
printf ' Rule: github-discipline.md Rule 5.1 (tool-agnostic)\n' >&2
|
||||
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'
|
||||
'this commit was generated'
|
||||
@ -52,7 +62,7 @@ LITERAL_PATTERNS=(
|
||||
'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
|
||||
printf 'commit-msg: forbidden AI-attribution phrase: %s\n' "$p" >&2
|
||||
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
||||
@ -88,48 +98,15 @@ for p in "${PREAMBLE_PATTERNS[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Rule 5.1: Cursor attribution. Reject Cursor co-author trailers, the agent
|
||||
# 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
|
||||
# Author / committer identity must not be an AI agent. git var resolves the exact
|
||||
# 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
|
||||
[ -z "$ident" ] && continue
|
||||
if printf '%s' "$ident" | grep -qiE -- '@cursor\.com|(^|[^[:alpha:]])cursor(agent)?([^[:alpha:]]|$)'; then
|
||||
printf 'commit-msg: forbidden Cursor identity as author/committer: %s\n' "$ident" >&2
|
||||
if printf '%s' "$ident" | grep -qiE -- "$IDENT_REGEX"; then
|
||||
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
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
@ -75,10 +75,8 @@ SKIP_PATTERNS=(
|
||||
'/pre-commit$'
|
||||
'/pre-push$'
|
||||
'/check-style\.sh$'
|
||||
'/check-commit-attribution\.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.
|
||||
|
||||
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
|
||||
|
||||
[](https://github.com/mihaelamj/PureXML/actions/workflows/ci.yml)
|
||||
[](https://github.com/mihaelamj/PureXML/actions/workflows/ci.yml)
|
||||
[](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/swift-macos.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.
|
||||
|
||||
@ -126,27 +129,66 @@ flowchart TB
|
||||
|
||||
## Status
|
||||
|
||||
The node model, the emitter, and a streaming parser are implemented and usable
|
||||
today.
|
||||
PureXML is a working, dependency-free XML library today: parse, emit, validate,
|
||||
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`.
|
||||
Preserves document order and the distinction between text, CDATA, comments,
|
||||
and processing instructions.
|
||||
- **Emitting** (`PureXML.Emitting`): `Serializer` turns a node tree into
|
||||
well-formed XML, with pretty-printed and compact options and correct text and
|
||||
attribute escaping.
|
||||
- **Validation** (`PureXML.Validation`): structural checks such as duplicate
|
||||
attribute names. Schema validation (DTD/XSD/RELAX NG) is out of scope for the
|
||||
library target.
|
||||
- **Parsing** (`PureXML.Parsing`): a streaming, iterative parser. `EventReader`
|
||||
is a pull-based event core that consumes input through a character-source
|
||||
closure and emits one `Event` at a time, never holding the whole document in
|
||||
memory (it drives chunked input). `Parser` builds a `Model.Node` tree
|
||||
iteratively over the event core. Handles elements, attributes, text, the five
|
||||
predefined entities and numeric character references, comments, CDATA, and
|
||||
processing instructions. Safe by default: `<!DOCTYPE>` is rejected, which
|
||||
removes the DTD-based threat classes (XXE, entity-expansion DoS). DTD support,
|
||||
namespaces-as-resolved-URIs, and validation are deliberate future layers.
|
||||
### Shipped (libxml2-aligned surface)
|
||||
|
||||
- **Model** (`PureXML.Model`): `Node`, `Element`, `Attribute`, `QualifiedName`,
|
||||
and a mutable `TreeNode` for in-place editing (insert, remove, replace, copy,
|
||||
parent navigation).
|
||||
- **Parsing** (`PureXML.Parsing`): iterative streaming parser over strings,
|
||||
bytes, or incremental character/byte sources. Pull events via `EventReader`,
|
||||
SAX2-style callbacks via `SAXHandler`, and a pull cursor via `TextReader`
|
||||
(libxml2 `xmlTextReader`). Handles elements, attributes, text, predefined and
|
||||
numeric entities, comments, CDATA, and processing instructions. Namespace
|
||||
bindings (`xmlns`) are resolved to URIs on parse.
|
||||
- **Encoding**: UTF-8/16/32 (with BOM sniffing), ISO-8859-1, and Windows-1252
|
||||
from raw bytes or streaming byte input.
|
||||
- **DTD (opt-in)**: `<!DOCTYPE>` is **off by default** (`Limits.allowDoctype:
|
||||
false`) to keep XXE and entity-expansion closed. When enabled, the internal
|
||||
subset is parsed; internal general entities expand with a bounded cap; external
|
||||
entities and external subsets are refused unless an injected
|
||||
`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
|
||||
|
||||
@ -171,6 +213,16 @@ var reader = PureXML.events(xml)
|
||||
while let event = try reader.next() {
|
||||
// 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
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
public extension PureXML {
|
||||
/// 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(
|
||||
_ xml: String,
|
||||
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.
|
||||
|
||||
## 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)
|
||||
|
||||
Load this rule when:
|
||||
@ -251,5 +299,6 @@ Load this rule when:
|
||||
- Running `git commit --amend` (Rule 5.3)
|
||||
- Running `git push` (Rule 6.1, Rule 6.2)
|
||||
- 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.
|
||||
|
||||
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.
|
||||
is_enforcement_file() {
|
||||
case "$1" in
|
||||
.githooks/*|scripts/check-style.sh) return 0 ;;
|
||||
.github/workflows/no-cursor-attribution.yml) return 0 ;;
|
||||
.githooks/*|scripts/check-style.sh|scripts/check-commit-attribution.sh) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user