From 0266f2b90df9a95cc9c89269e54595382f9cc64c Mon Sep 17 00:00:00 2001 From: Shankar Date: Mon, 27 Apr 2026 02:55:35 +0000 Subject: [PATCH] M-029 Pass 1 closure: tighten ci.yml M-009 guard from soft budget to hard zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass 1 finished — every src/ useMutation now goes through useTrackedMutation. Promote the M-009 guard to a hard-zero invariant: any bare useMutation() call outside web/src/hooks/useTrackedMutation.ts fails CI immediately. Pre-Bundle-8 the codebase had 56 bare useMutation sites. Bundle 8 shipped the wrapper. M-029 Pass 1 migrated all 56 sites to the wrapper across 6 batches (commits 08ffbad / 73c6883 / 64c6cd0 / d5541fe / 1c960ff / 1baefd4). With the soft-budget gate now obsolete, the hard-zero gate prevents drift back into the discretionary-invalidation pattern that motivated M-009 in the first place. Rationale: per-site enforcement (the wrapper's discriminated-union invalidates contract) is strictly stronger than the +5 budget guard. The guard's failure mode also improves: instead of a count delta the operator has to interpret, they get the exact file:line(s) of the offending bare useMutation call. Verification: python3 yaml.safe_load YAML OK manual guard simulation PASS: bare useMutation = 0 outside wrapper --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d86dba..f9f6125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1003,32 +1003,42 @@ jobs: fi echo "L-019 dangerouslySetInnerHTML guardrail: clean." - - name: Bundle-8 / M-009 mutation invalidation contract guard - # Audit M-009: every useMutation must either invalidate the - # queries it changes OR document why no invalidation is needed. - # SOFT guard — counts useMutation sites and asserts the budget - # doesn't grow without a corresponding invalidateQueries / setQueryData / - # useTrackedMutation reference. Stricter per-site enforcement is - # tracked as M-029 (covers the long-tail useListParams + useTrackedMutation - # migration of the existing 56 useMutation sites). + - name: Bundle-8 / M-009 + M-029 Pass 1 mutation contract guard (hard zero) + # Audit M-009 + M-029 Pass 1 closure: + # + # Pre-Bundle-8 the codebase had 56 bare useMutation sites with + # discretionary invalidation. Bundle 8 shipped the useTrackedMutation + # wrapper (web/src/hooks/useTrackedMutation.ts) that requires every + # caller to declare `invalidates: QueryKey[] | 'noop'`. M-029 Pass 1 + # then migrated all 56 sites to the wrapper across 6 batches. + # + # This guard pins the contract going forward: every useMutation call + # in src/ MUST be inside useTrackedMutation.ts (the wrapper itself + # is the only legitimate caller of useMutation). Any bare useMutation + # call elsewhere is a regression — adding a new mutation site means + # going through the wrapper so the invalidates contract is enforced + # per-site, not by a soft budget guard. + # + # If you genuinely need raw useMutation (extremely unlikely — the + # wrapper supports invalidates: 'noop' for fire-and-forget mutations), + # update this guard's exclusion list and document the carve-out. run: | set -e - MUTATIONS=$(grep -rcE 'useMutation\(|useTrackedMutation\(' web/src/ 2>/dev/null \ - | awk -F: '{s+=$2} END{print s}') - INVALIDATIONS=$(grep -rcE 'invalidateQueries|setQueryData|removeQueries|invalidates:' web/src/ 2>/dev/null \ - | awk -F: '{s+=$2} END{print s}') - echo "M-009 budget — useMutation sites: $MUTATIONS / invalidation sites: $INVALIDATIONS" - # At Bundle-8 close: 56 useMutation + 70 invalidation. We allow - # +5 mutations growth before requiring invalidation parity. If - # the gap widens, audit the new mutation sites for missing - # invalidation pairs. - BUDGET=$((INVALIDATIONS + 5)) - if [ "$MUTATIONS" -gt "$BUDGET" ]; then - echo "M-009 regression: $MUTATIONS useMutation sites exceeds invalidation budget ($BUDGET)." - echo "New mutations should pair with invalidateQueries/setQueryData OR migrate to" - echo "useTrackedMutation (web/src/hooks/useTrackedMutation.ts) with explicit invalidates." + BARE=$(grep -rnE '\buseMutation\(' web/src/ 2>/dev/null | grep -v 'web/src/hooks/useTrackedMutation\.ts' || true) + if [ -n "$BARE" ]; then + echo "M-009 hard-zero regression: bare useMutation() call(s) outside the wrapper:" + echo "$BARE" + echo + echo "Every mutation must go through useTrackedMutation" + echo "(web/src/hooks/useTrackedMutation.ts) with explicit" + echo "invalidates: QueryKey[] | 'noop'. See file header for usage." exit 1 fi + # Sanity counts (informational, not a gate). + TRACKED=$(grep -rcE '\buseTrackedMutation\(' web/src/ 2>/dev/null | awk -F: '{s+=$2} END{print s}') + INVALIDATIONS=$(grep -rcE 'invalidateQueries|setQueryData|removeQueries|invalidates:' web/src/ 2>/dev/null | awk -F: '{s+=$2} END{print s}') + echo "M-009 hard-zero: bare useMutation sites = 0 (wrapper-internal call excluded)." + echo "M-009 informational: useTrackedMutation sites = $TRACKED; invalidation surface = $INVALIDATIONS." - name: Forbidden env-var docs drift regression guard (G-3) # G-3 master closed cat-g-163dae19bc59 (docs-only env vars