diff --git a/.github/workflows/repo_checks.yml b/.github/workflows/repo_checks.yml new file mode 100644 index 000000000..955e6ddd1 --- /dev/null +++ b/.github/workflows/repo_checks.yml @@ -0,0 +1,63 @@ +############################################################################### +# Copyright (c) 2026 Eclipse ThreadX contributors +# +# This program and the accompanying materials are made available under the +# terms of the MIT License which is available at +# https://opensource.org/licenses/MIT. +# +# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). +# The AI-generated portions may be considered public domain (CC0-1.0) +# and not subject to the project's licence. The human contributor has +# reviewed and verified that the code is correct. +# +# SPDX-License-Identifier: MIT and CC0-1.0 +############################################################################### + +name: repo_checks + +# Repository-wide text checks. +# +# THIS LIVES IN ITS OWN WORKFLOW BECAUSE IT MUST NOT BE PATH-FILTERED, and a +# path filter is a property of a workflow rather than of a job. The check +# selects its input with `git ls-files` and scans every tracked file, so any +# file at all can carry a finding. +# +# That is not hypothetical here. The pass that introduced the fixed +# disclosure line covered source files only, and the drift it was meant to end +# survived for months afterwards in CMake files, toolchain files, shell and +# PowerShell scripts, a GDB script and a Visual Studio manifest -- precisely +# the files a source-path filter would have skipped. A workflow that gates no +# pull request anybody opens is worse than no workflow, because it looks like +# coverage. +# +# Cheap enough that running it on everything costs nothing worth measuring: it +# is grep over a repository this size, with no toolchain, no build and no +# cache. + +on: + # No `paths:` on either trigger, deliberately. See above. + push: + branches: [ dev, master ] + pull_request: + branches: [ dev, master ] + +# A second push to the same branch makes the first answer irrelevant. +concurrency: + group: repo-checks-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-24.04 + steps: + # Actions are pinned to a commit SHA with the version in the trailing + # comment. A tag can be moved; a SHA cannot, which is what makes "which + # code ran in our CI" answerable from the repository. + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check the AI disclosure comments + run: scripts/check_ai_disclosure.sh diff --git a/scripts/check_ai_disclosure.sh b/scripts/check_ai_disclosure.sh new file mode 100755 index 000000000..d9707ac95 --- /dev/null +++ b/scripts/check_ai_disclosure.sh @@ -0,0 +1,120 @@ +#!/bin/bash +############################################################################### +# Copyright (c) 2026 Eclipse ThreadX contributors +# +# This program and the accompanying materials are made available under the +# terms of the MIT License which is available at +# https://opensource.org/licenses/MIT. +# +# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). +# The AI-generated portions may be considered public domain (CC0-1.0) +# and not subject to the project's licence. The human contributor has +# reviewed and verified that the code is correct. +# +# SPDX-License-Identifier: MIT and CC0-1.0 +############################################################################### +# +# Fail the build if a file's AI disclosure comment has drifted from the one +# accepted form. +# +# A file that was edited with AI assistance carries exactly one line: +# +# Portions of this file were generated with AI assistance. +# +# written with the comment character that file already uses. It names no +# product, no model and no version, and a file carries at most one of it, ever. +# +# The text is fixed for a reason that is easy to miss. An earlier convention +# named the product and the model -- "Some portions generated by +# ()" -- and the result was not attribution but accumulation: each tool +# that touched a file failed to recognise the line another tool had left, and +# appended its own. Files reached three stacked lines, and one product ended +# up spelled four different ways across the tree, which made the record +# unusable for the one question it was meant to answer. +# +# Precise attribution lives on the commit instead, where the Assisted-by +# trailer is dated and attached to the diff it describes: +# +# git log --format='%h %(trailers:key=Assisted-by,valueonly)' -- +# +# A file-level flag answers WHETHER; the history answers WHO. A header line +# cannot hold the second honestly, because the code it names gets rewritten +# and the line stays. +# +# The AI Disclosure paragraph in a new file's copyright header is different and +# is not checked here: it keeps its product and model version, because a file +# is created once and that record cannot grow. +# +# This script is excluded from its own scan. It has to spell the rejected +# forms in order to look for them. + +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)" +readonly SELF='scripts/check_ai_disclosure.sh' +readonly FIXED='Portions of this file were generated with AI assistance.' + +cd "${ROOT}" + +# Tracked files only. A build tree is not this repository's text to police, +# and scanning one would make the check depend on whether somebody had built. +mapfile -d '' -t FILES < <(git ls-files -z | grep -zZv "^${SELF}$") + +status=0 + +report() { + printf '%s\n\n' "$1" >&2 + printf '%s\n\n' "$2" >&2 + status=1 +} + +# 1. The superseded per-edit form, which names a product and a model. +hits="$(grep -nI 'Some portions generated by' -- "${FILES[@]}" 2>/dev/null || true)" +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: superseded per-edit form. + +Replace each of these with the fixed line, keeping the file's comment +character: + + ${FIXED}" "${hits}" +fi + +# 2. A doubled comment marker, such as '; //' or '@ //'. Assembly dialects +# differ -- armasm and IAR use ';', GNU as uses '@' or '//' -- and writing +# both is a symptom of a tool guessing rather than reading the file. +hits="$(grep -nIE '(//|[;@#])[[:space:]]*//[[:space:]]*Portions of this file were generated' \ + -- "${FILES[@]}" 2>/dev/null || true)" +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: doubled comment marker. + +Use the single comment character the rest of the file uses." "${hits}" +fi + +# 3. More than one disclosure line in a file. This is the failure the fixed +# text exists to prevent, so it is worth catching directly rather than +# inferring it from the form. +hits="$(grep -cIF "${FIXED}" -- "${FILES[@]}" 2>/dev/null | awk -F: '$NF > 1' || true)" +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: more than one disclosure line. + +A file carries at most one, ever. Keep the first and delete the rest; the +commit trailer, not the header, records which agents have touched the file." "${hits}" +fi + +# 4. A near miss. A line that is clearly meant to be the disclosure but is +# not spelled exactly right defeats every deduplication that follows it. +hits="$(grep -nIF 'AI assistance' -- "${FILES[@]}" 2>/dev/null \ + | grep -vF "${FIXED}" || true)" +if [ -n "${hits}" ]; then + report "AI disclosure check FAILED: the text is not spelled exactly. + +The accepted text, character for character, is: + + ${FIXED}" "${hits}" +fi + +if [ "${status}" -eq 0 ]; then + echo "AI disclosure check passed." +fi + +exit "${status}"