From 51b358828686bdefe31195c34efc2c2686926084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Labont=C3=A9?= Date: Wed, 28 Oct 2026 11:19:58 -0400 Subject: [PATCH] ci(example-regen): run the C# starter contract on real generator output The starter contract was only ever proven on a synthetic tree. example-regen now seeds it into the component the add-component step generates with the pinned jar, and runs the contract, the patch check and the mutation test there (skips allowed for surfaces hello-orders does not generate, misses not). Running it locally against 0.19.0 output found two defects, fixed here: - GEN-021 flagged the comment in add-component's own seeded .gitignore that says the manifest is deliberately not ignored. It now matches ignore lines only, and the synthetic seed carries the same comment so it stays covered. - The GEN-042 mutation hard-coded IOrderPlacedHandler, which exists only in the synthetic tree, so it reported a miss on any real repo without that handler. It now derives the interface from a generated *HandlerBase and skips, like GEN-040 and GEN-041, when there is none. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/example-regen.yml | 30 +++++++++++++++++++ scripts/test-csharp-contract-template.sh | 1 + .../csharp/gen-contract.yml | 3 +- .../csharp/scripts/test-gen-contract.sh | 9 +++++- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/example-regen.yml b/.github/workflows/example-regen.yml index fc48911..942ba18 100644 --- a/.github/workflows/example-regen.yml +++ b/.github/workflows/example-regen.yml @@ -1605,6 +1605,36 @@ jobs: fi echo "==> add-component csharp: seeds, builds, and serves on the pinned generator." + - name: The C# starter contract holds on real generator output + if: steps.token.outputs.have == 'true' + # ci.yml proves the starter contract on a synthetic tree. This runs it + # where it will actually be used: the component the previous step + # seeded and regenerated with the pinned jar. A pin whose output breaks + # a marker, layering or wiring rule fails here, not in an adopter's + # first regen PR. Skips are expected for rules whose surface hello-orders + # does not generate (handler and application-service bases); a miss is + # a rule that cannot fail on real output. + run: | + set -euo pipefail + python3 -m pip install --quiet 'specfuse-loop>=0.30' + cd "$RUNNER_TEMP/component-cs/Backend" + KIT="$GITHUB_WORKSPACE/templates/component-contract/csharp" + cp "$KIT/gen-contract.yml" . + mkdir -p scripts docs gen-patches + cp "$KIT/scripts/"* scripts/ + cp "$KIT/docs/generated-code-expectations.md" docs/ + cp "$KIT/gen-patches/README.md" gen-patches/ + sed -i 's/__Prefix__/HelloOrders/g' gen-contract.yml docs/generated-code-expectations.md + chmod +x scripts/*.sh + git -c user.email=ci@example.invalid -c user.name=ci add -A + git -c user.email=ci@example.invalid -c user.name=ci commit -q -m "seed and regen" + ./scripts/check-gen-contract.sh + ./scripts/apply-gen-patches.sh --check + out="$(./scripts/test-gen-contract.sh)" || { printf '%s\n' "$out"; exit 1; } + printf '%s\n' "$out" + grep -q ' 0 missed' <<<"$out" || { echo "::error::a starter rule did not bite on real generator output"; exit 1; } + echo "==> the C# starter contract is green on the pinned generator's output, and its rules bite there." + - name: A seeded Python component installs, imports and passes its tests (add-component drift guard, #177) if: steps.token.outputs.have == 'true' # The project is renamed PascalCase in this copy only: the generator diff --git a/scripts/test-csharp-contract-template.sh b/scripts/test-csharp-contract-template.sh index 6b50dfc..7d5772c 100755 --- a/scripts/test-csharp-contract-template.sh +++ b/scripts/test-csharp-contract-template.sh @@ -67,6 +67,7 @@ EOF cat > .gitignore <<'EOF' bin/ obj/ +# *.generated-paths.json is deliberately NOT ignored (the seed's own comment). EOF header() { diff --git a/templates/component-contract/csharp/gen-contract.yml b/templates/component-contract/csharp/gen-contract.yml index efdf47f..903cc91 100644 --- a/templates/component-contract/csharp/gen-contract.yml +++ b/templates/component-contract/csharp/gen-contract.yml @@ -267,7 +267,8 @@ rules: the repo and a layout move reads as an unexplained delete plus add. check: forbid_pattern scope: gitignore - pattern: 'generated-paths' + # A comment saying the omission is deliberate is fine; an ignore line is not. + pattern: '^[^#]*generated-paths' allow_empty: true # ── Hand-written code holds up its end ───────────────────────────────── diff --git a/templates/component-contract/csharp/scripts/test-gen-contract.sh b/templates/component-contract/csharp/scripts/test-gen-contract.sh index 0a63d1d..3e18145 100755 --- a/templates/component-contract/csharp/scripts/test-gen-contract.sh +++ b/templates/component-contract/csharp/scripts/test-gen-contract.sh @@ -146,7 +146,14 @@ if [ -n "$APPSVC_BASE" ]; then else echo " SKIP GEN-041 — no *ApplicationServiceBase.g.cs in the tree yet"; skip=$((skip+1)) fi -expect_fail_newfile GEN-042 "${SRC_CS%/*}/DirectHandler.cs" "public sealed class DirectHandler : IOrderPlacedHandler { }" +if [ -n "$HANDLER_BASE" ]; then + # The interface beside the base: OrderPlacedHandlerBase.g.cs declares IOrderPlacedHandler. + HANDLER_IFACE="I$(basename "$HANDLER_BASE" .g.cs)" + HANDLER_IFACE="${HANDLER_IFACE%Base}" + expect_fail_newfile GEN-042 "${SRC_CS%/*}/DirectHandler.cs" "public sealed class DirectHandler : $HANDLER_IFACE { }" +else + echo " SKIP GEN-042 — no generated handler interface in the tree yet"; skip=$((skip+1)) +fi echo echo " $pass ok, $fail missed, $skip skipped"