From e9d4240cf3427cbb26bd005886ee44261e1282bc Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 18:17:02 +0200 Subject: [PATCH 1/4] ci: add header <-> spec sync gate New required PR check: a change under include/morph// must be accompanied by a change under docs/spec// (folders mirror each other). detail/ and qt/ are exempt (no specs). The gate is skipped when the PR carries the label 'no docs update'. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/spec-sync.yml | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/spec-sync.yml diff --git a/.github/workflows/spec-sync.yml b/.github/workflows/spec-sync.yml new file mode 100644 index 0000000..85886ef --- /dev/null +++ b/.github/workflows/spec-sync.yml @@ -0,0 +1,72 @@ +name: Spec sync + +# Enforces that a change to a header sub-domain (include/morph//**) is +# accompanied by a change to the matching design-spec folder (docs/spec//**), +# so the specs cannot silently drift behind the code they describe. +# +# The gate is skipped when the PR carries the label "no docs update" — a visible, +# reviewable opt-out for changes that genuinely need no spec update. + +on: + pull_request: + branches: + - master + # `labeled`/`unlabeled` so adding or removing the escape-hatch label re-runs + # the gate instead of leaving a stale result. + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + +jobs: + spec-sync: + name: Header ↔ spec sync + runs-on: ubuntu-24.04 + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no docs update') }} + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Enforce header ↔ spec synchronization + env: + BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + + git fetch --no-tags --depth=1 origin "$BASE_REF" + base="$(git merge-base HEAD "origin/$BASE_REF")" + echo "Comparing against merge-base $base (origin/$BASE_REF)" + + changed="$(git diff --name-only "$base" HEAD)" + echo "── Changed files ──" + echo "$changed" + echo "───────────────────" + + # Sub-domains that have a mirrored spec folder. include/morph/detail/** + # and include/morph/qt/** have no specs and are intentionally exempt. + subdomains="core journal offline session forms util" + + missing="" + for sub in $subdomains; do + if echo "$changed" | grep -qE "^include/morph/$sub/"; then + if ! echo "$changed" | grep -qE "^docs/spec/$sub/"; then + missing="$missing $sub" + fi + fi + done + + if [ -n "$missing" ]; then + echo "" + echo "::error::Header changes without a matching spec update in:$missing" + for sub in $missing; do + echo " - include/morph/$sub/** changed but docs/spec/$sub/** did not." + done + echo "" + echo "Update the design spec(s) for the affected sub-domain(s), or add the" + echo "label 'no docs update' to this PR if no spec change is warranted." + exit 1 + fi + + echo "Spec sync OK: every touched header sub-domain has a matching spec change." From 7ddb211455db4c7aedc6dd8797eabaf49265cce2 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 18:18:19 +0200 Subject: [PATCH 2/4] test: core header change with no matching spec (expect gate FAIL) Co-Authored-By: Claude Opus 4.8 (1M context) --- include/morph/core/_gate_selftest.hpp | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 include/morph/core/_gate_selftest.hpp diff --git a/include/morph/core/_gate_selftest.hpp b/include/morph/core/_gate_selftest.hpp new file mode 100644 index 0000000..7fe737d --- /dev/null +++ b/include/morph/core/_gate_selftest.hpp @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once +// Throwaway fixture to exercise the spec-sync CI gate. Not part of the library. From 2fa25e432229ed88395b5f1899521b1835893235 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 18:19:16 +0200 Subject: [PATCH 3/4] test: add matching core spec (expect gate PASS) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/core/_gate_selftest.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/spec/core/_gate_selftest.md diff --git a/docs/spec/core/_gate_selftest.md b/docs/spec/core/_gate_selftest.md new file mode 100644 index 0000000..30eccd9 --- /dev/null +++ b/docs/spec/core/_gate_selftest.md @@ -0,0 +1,4 @@ +# _gate_selftest (throwaway) + +Fixture spec paired with `include/morph/core/_gate_selftest.hpp` to exercise the +spec-sync gate's pass path. From 3ebb74cd1f82019eedf41bf4601daf4a8d355f1f Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 18:20:07 +0200 Subject: [PATCH 4/4] test: remove spec again (header-only, expect FAIL without label) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/core/_gate_selftest.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 docs/spec/core/_gate_selftest.md diff --git a/docs/spec/core/_gate_selftest.md b/docs/spec/core/_gate_selftest.md deleted file mode 100644 index 30eccd9..0000000 --- a/docs/spec/core/_gate_selftest.md +++ /dev/null @@ -1,4 +0,0 @@ -# _gate_selftest (throwaway) - -Fixture spec paired with `include/morph/core/_gate_selftest.hpp` to exercise the -spec-sync gate's pass path.