From e1825aabbfa36a52bcb1811a2c45ec11026b8b5d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 07:30:37 +0000 Subject: [PATCH] ci(lint): run check:migration-registry in Lint & Repo Gates, scoped by the gate-family selector registry.ts is the generated concatenation of packages/spec/src/migrations/ entries/** and ships in the spec package's dist; check:migration-registry is the only proof the two agree and it ran in no CI step. It becomes a step in the required Lint & Repo Gates job, behind a new migration_registry family of scripts/ci/select-gate-families.sh whose read-set is the migration tree, the generator, and the spec package.json and tsconfig.json. The selector's fail-open branches run it like every other family; the self-test pins the family's verdicts and the workflow step. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude --- .github/workflows/lint.yml | 25 +++- scripts/ci/select-gate-families.selftest.sh | 138 ++++++++++++++++++-- scripts/ci/select-gate-families.sh | 40 +++++- 3 files changed, 186 insertions(+), 17 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b10dd42b206..0270086d309 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -248,13 +248,15 @@ jobs: # over the ten merge-group runs measured for #16496; the PM dispatch-gates # self-test alone 597 s), and none of its expensive steps read a # merge group's file surface: a docs-only group paid the full battery. - # So on `merge_group` and `pull_request` the NINE scoped families below + # So on `merge_group` and `pull_request` the TEN scoped families below # -- each step carrying `if: steps.gate-families.outputs. != 'skip'` # -- run only when the changed paths touch the files that family reads. + # One of the ten, `migration_registry` (#19753), is a gate this job did + # not run before at all; the other nine were scoped away from it. # `push` on main and the scheduled full run keep the whole battery: the # script runs everything for any event it does not scope. # - # #19498 added four of those nine (`entry_guard`, + # #19498 added four of the nine (`entry_guard`, # `declared_population_live`, `bare_root_worklist`, # `self_test_workflow_commands`) and narrowed `pm_dispatch_gates`, on # ruling #208 on #19491 (R4) and the maintainer's sentence quoted in the @@ -362,6 +364,25 @@ jobs: - name: ADR anchors + number uniqueness (governed code names its decision) run: pnpm check:adr-anchors + # Migration registry matches its entry files (#19753). `registry.ts` is + # the generated concatenation of `packages/spec/src/migrations/entries/`, + # it ships in the spec package's `dist`, and this check is the only + # proof the two agree -- `check:generated --reconcile-only` (in the + # `Type Check · source gates` job) proves the script is classified, + # never that the artifact is current. It ran + # in no CI step, so an entry edited without `gen:migration-registry` + # shipped the old text with every required check green. + # + # Scoped, on the maintainer's ruling for that card: a step in THIS + # required job, run when the change touches what the check reads + # (`migration_registry` in the selector above). ⛔ Not a separate + # `paths:`-filtered job: one that never reports on an unrelated PR + # leaves a required context pending forever. Cheap -- tsx over one + # directory, no build (~1.4 s measured locally) -- so it sits early. + - name: Migration registry matches its entry files + if: steps.gate-families.outputs.migration_registry != 'skip' + run: pnpm --filter @objectstack/spec check:migration-registry + # Enforces the no-restricted-imports guard against @objectstack/spec root # namespace imports (the dormant rule was never run in CI). Syntactic # only, so no build step needed. diff --git a/scripts/ci/select-gate-families.selftest.sh b/scripts/ci/select-gate-families.selftest.sh index ddc3c45e4be..9b9b462a130 100644 --- a/scripts/ci/select-gate-families.selftest.sh +++ b/scripts/ci/select-gate-families.selftest.sh @@ -20,6 +20,12 @@ # edit, an added test, an added changeset) must skip every tooling self-test, # while the tool's own inputs must still run them. # +# Since #19753 it covers migration_registry, the one family that ADDS a gate +# to the PR path: an entry, the generated registry, the generator and the two +# package files that decide how it runs must each select it; a spec source +# outside the migration tree, a sibling spec script and a lookalike directory +# must not; and a deletion, an empty diff or an unresolvable base still run it. +# # The last section reads the REAL lint.yml and pins the YAML half of the # contract: the selector step exists under the id the `if:` lines name, every # scoped step spells `!= 'skip'` (an absent output runs the step), and the set @@ -57,7 +63,7 @@ git_q() { git -c user.name=selftest -c user.email=selftest@example.invalid -c commit.gpgsign=false "$@" } -ALL='slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock' +ALL='migration_registry slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock' # ── Fixture repositories ──────────────────────────────────────────────────── # C0 carries one representative file of every class the classifier names, so @@ -67,7 +73,8 @@ mkdir -p "$UP" git_q -C "$UP" init -q git_q -C "$UP" symbolic-ref HEAD refs/heads/main mkdir -p "$UP/packages/a/scripts" "$UP/packages/a/src" "$UP/apps/site/src" "$UP/docs" "$UP/content/docs" \ - "$UP/scripts/pm" "$UP/scripts/ci" "$UP/.github/workflows" "$UP/.claude/agents" "$UP/skills/x" "$UP/.changeset" + "$UP/scripts/pm" "$UP/scripts/ci" "$UP/.github/workflows" "$UP/.claude/agents" "$UP/skills/x" "$UP/.changeset" \ + "$UP/packages/spec/src/migrations/entries/semantic" "$UP/packages/spec/scripts" printf '{"name":"fixture","private":true}\n' > "$UP/package.json" printf 'packages:\n - packages/*\n' > "$UP/pnpm-workspace.yaml" printf '{"name":"a"}\n' > "$UP/packages/a/package.json" @@ -92,6 +99,14 @@ printf '# skill\n' > "$UP/skills/x/SKILL.md" printf '# rules\n' > "$UP/AGENTS.md" printf '# readme\n' > "$UP/README.md" printf -- '---\n"a": patch\n---\nchange\n' > "$UP/.changeset/first.md" +# The migration_registry read-set (#19753), and its nearest neighbours outside it. +printf '{"name":"spec"}\n' > "$UP/packages/spec/package.json" +printf '{}\n' > "$UP/packages/spec/tsconfig.json" +printf 'export const index = 1;\n' > "$UP/packages/spec/src/index.ts" +printf 'export const registry = [];\n' > "$UP/packages/spec/src/migrations/registry.ts" +printf 'export const entry = 1;\n' > "$UP/packages/spec/src/migrations/entries/semantic/17.x.ts" +printf 'export const gen = 1;\n' > "$UP/packages/spec/scripts/build-migration-registry.ts" +printf 'export const schemas = 1;\n' > "$UP/packages/spec/scripts/build-schemas.ts" git_q -C "$UP" add -A git_q -C "$UP" commit -q -m 'C0: root' C0=$(git_q -C "$UP" rev-parse HEAD) @@ -254,7 +269,7 @@ cases=$((cases + 1)); RT="$FIX/rt-$cases"; mkdir -p "$RT" rc=$? echo "case: --families prints the family ids in job order" expect_rc 0 -expect_file_is 'the nine ids' "$RT/out.txt" "$(printf '%s\n' $ALL)" +expect_file_is 'the ten ids' "$RT/out.txt" "$(printf '%s\n' $ALL)" # ── events that are not scoped ────────────────────────────────────────────── S=$(scenario M:docs/guide.md) @@ -265,7 +280,8 @@ expect_all_run expect_reason pm_dispatch_gates "event 'push' is not scoped" expect_reason entry_guard "event 'push' is not scoped" expect_reason bare_root_worklist "event 'push' is not scoped" -expect_line 'Gate families: 9 run, 0 skipped' +expect_reason migration_registry "event 'push' is not scoped" +expect_line 'Gate families: 10 run, 0 skipped' run_case 'schedule: every family runs (the hourly full run keeps the battery)' "$REPO" schedule '' '' expect_rc 0 @@ -291,7 +307,7 @@ expect_reason pm_dispatch_gates 'scripts/pm/tool.mjs (M, scripts)' expect_reason self_test_workflow_commands 'scripts/pm/tool.mjs (M, scripts)' expect_changed 'one M' "M scripts/pm/tool.mjs" expect_line "Gate-family diff base: $C0 (the merge group's base_sha)" -expect_line 'Gate families: 6 run, 3 skipped' +expect_line 'Gate families: 6 run, 4 skipped' S=$(scenario A:scripts/pm/new-tool.mjs) run_case 'merge_group: an ADDED file inside the read-set still runs it -- the narrowing is about the class, never the status' "$REPO" merge_group '' "$C0" @@ -305,7 +321,7 @@ run_case 'merge_group: a scripts/pm prose change runs the tooling self-tests, an expect_rc 0 expect_warnings '' '' expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands -expect_line 'Gate families: 5 run, 4 skipped' +expect_line 'Gate families: 5 run, 5 skipped' S=$(scenario M:docs/guide.md M:content/docs/page.mdx M:.changeset/first.md M:README.md) run_case 'merge_group: a docs-only group skips every family, and prints it' "$REPO" merge_group '' "$C0" @@ -314,11 +330,13 @@ expect_warnings '' '' expect_verdicts expect_reason pm_dispatch_gates 'no changed path is in its read-set' expect_reason entry_guard 'no changed path is in its read-set' -expect_line 'Gate families: 0 run, 9 skipped' +expect_reason migration_registry 'no changed path is in its read-set' +expect_line 'Gate families: 0 run, 10 skipped' expect_line 'skip pm_dispatch_gates' expect_line 'skip comment_mask_corpus' expect_line 'skip self_test_workflow_commands' -if grep -q '^| `pm_dispatch_gates` | skip |' "$RT/step-summary" && grep -q '^## Gate families: 0 run, 9 skipped' "$RT/step-summary"; then +expect_line 'skip migration_registry' +if grep -q '^| `pm_dispatch_gates` | skip |' "$RT/step-summary" && grep -q '^## Gate families: 0 run, 10 skipped' "$RT/step-summary"; then ok 'the step summary lists the skipped families' else bad 'the step summary lists the skipped families' "$(tr '\n' ' ' < "$RT/step-summary")" @@ -380,6 +398,7 @@ run_case 'merge_group: an EMPTY diff runs every family rather than selecting not expect_rc 0 expect_warnings '#10057' "::warning::The diff against $C0 listed no changed files; every gate family runs rather than selecting nothing (#10057)." expect_all_run +expect_reason migration_registry "the diff against $C0 listed no changed files" expect_changed 'empty' '' S=$(scenario M:docs/guide.md) @@ -388,12 +407,14 @@ expect_rc 0 expect_warnings 'no base' '::warning::This merge_group event carries no base_sha, so the gate-family diff base cannot be computed; every family runs.' expect_all_run expect_reason pm_dispatch_gates 'no base_sha in the merge_group event' +expect_reason migration_registry 'no base_sha in the merge_group event' run_case 'merge_group: an unfetchable base_sha warns twice and runs everything' "$REPO" merge_group '' "$ZEROS" expect_rc 0 expect_warnings 'fetch + resolve' "::warning::Could not fetch the merge group's base $ZEROS; the resolution below will decide. ::warning::Could not resolve the merge group's base '$ZEROS' in this checkout; every gate family runs rather than guessing which paths changed (#16453)." expect_all_run +expect_reason migration_registry "merge group base '$ZEROS' unresolvable" if git_q -C "$SHALLOW" cat-file -e "$C0^{commit}" 2>/dev/null; then bad 'precondition: the shallow clone lacks C0 before the fetch case' @@ -442,7 +463,8 @@ expect_reason declared_population_live 'no changed path is in its read-set' expect_reason bare_root_worklist 'no changed path is in its read-set' expect_reason self_test_workflow_commands 'no changed path is in its read-set' expect_reason entry_guard 'no changed path is in its read-set' -expect_line 'Gate families: 3 run, 6 skipped' +expect_reason migration_registry 'no changed path is in its read-set' +expect_line 'Gate families: 3 run, 7 skipped' S=$(scenario M:apps/site/src/page.tsx) run_case 'merge_group: an apps TSX edit is outside the ratchets (packages/** only) and inside the corpus alone' "$REPO" merge_group '' "$C0" @@ -487,10 +509,11 @@ expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root expect_reason verify_lock '(M, verify-lock)' S=$(scenario M:scripts/helper.mjs) -run_case 'merge_group: a top-level scripts module is imported by the ratchets AND the lock preflight, and is a masked source' "$REPO" merge_group '' "$C0" +run_case 'merge_group: a top-level scripts module is imported by the ratchets AND the lock preflight, and is a masked source -- every family but the migration registry, which reads no repo-level script' "$REPO" merge_group '' "$C0" expect_rc 0 -expect_all_run +expect_verdicts slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock expect_reason verify_lock scripts/helper.mjs +expect_reason migration_registry 'no changed path is in its read-set' S=$(scenario M:scripts/slot-lookup-baseline.json) run_case 'merge_group: a ratchet baseline runs the ratchets and every family that reads scripts/' "$REPO" merge_group '' "$C0" @@ -523,7 +546,81 @@ S=$(scenario M:docs/guide.md M:packages/a/src/index.test.ts M:.github/workflows/ run_case 'merge_group: a mixed group runs the union of what its paths reach' "$REPO" merge_group '' "$C0" expect_rc 0 expect_verdicts slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands -expect_line 'Gate families: 7 run, 2 skipped' +expect_line 'Gate families: 7 run, 3 skipped' + +# ── merge_group: the migration registry family (#19753) ──────────────────── +# The #19523 shape first: an entry edited and the registry NOT regenerated. +# The entry is TypeScript under packages/, so the two ratchets and the corpus +# walk ride along; what these cases pin is the migration_registry verdict. +S=$(scenario M:packages/spec/src/migrations/entries/semantic/17.x.ts) +run_case 'merge_group: an entry edited without the registry (the #19523 shape) runs the migration registry' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus +expect_reason migration_registry packages/spec/src/migrations/entries/semantic/17.x.ts +expect_reason migration_registry '(M, workspace)' +expect_line 'Gate families: 4 run, 6 skipped' + +S=$(scenario A:packages/spec/src/migrations/entries/retired-keys/17.ui__X__y.ts) +run_case 'merge_group: an ADDED entry runs it -- a new directory under the tree is still the tree' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus +expect_reason migration_registry '(A, workspace)' + +S=$(scenario A:packages/spec/src/migrations/entries/semantic/notes.txt) +run_case 'merge_group: a non-TypeScript file in an entry directory runs it and nothing else -- the gate refuses a stray file there' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts migration_registry +expect_line 'Gate families: 1 run, 9 skipped' + +S=$(scenario M:packages/spec/src/migrations/registry.ts) +run_case 'merge_group: the generated registry edited alone runs it -- a hand edit is the other half of the drift' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus + +S=$(scenario M:packages/spec/scripts/build-migration-registry.ts) +run_case 'merge_group: the generator runs it, and every family that reads a package-local script' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason migration_registry packages/spec/scripts/build-migration-registry.ts + +S=$(scenario M:packages/spec/scripts/build-schemas.ts) +run_case 'merge_group: a SIBLING spec script is not the generator -- the arm names one file' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason migration_registry 'no changed path is in its read-set' + +S=$(scenario M:packages/spec/package.json) +run_case 'merge_group: the spec manifest runs it -- pnpm resolves the check script through it' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts migration_registry pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock +expect_reason migration_registry '(M, workspace)' + +S=$(scenario M:packages/spec/tsconfig.json) +run_case 'merge_group: the spec tsconfig runs it alone -- tsx loads it from the package directory' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts migration_registry +expect_line 'Gate families: 1 run, 9 skipped' + +S=$(scenario M:packages/spec/src/index.ts) +run_case 'merge_group: a spec source OUTSIDE the migration tree skips it' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts slot_lookup query_options_erasure comment_mask_corpus +expect_reason migration_registry 'no changed path is in its read-set' + +S=$(scenario A:packages/spec/src/migrations-notes/README.md) +run_case 'merge_group: a lookalike directory beside the tree skips it -- the arm is anchored on the separator' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts +expect_reason migration_registry 'no changed path is in its read-set' + +S=$(scenario D:packages/spec/src/migrations/entries/semantic/17.x.ts) +run_case 'merge_group: a DELETED entry runs it, with everything else (structural)' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_all_run +expect_reason migration_registry 'structural change (D packages/spec/src/migrations/entries/semantic/17.x.ts)' # ── pull_request ──────────────────────────────────────────────────────────── git_q -C "$REPO" checkout -q -B feature "$C0" @@ -534,6 +631,7 @@ run_case 'pull_request: decided against merge-base(origin/main, HEAD)' "$REPO" p expect_rc 0 expect_warnings '' '' expect_verdicts +expect_reason migration_registry 'no changed path is in its read-set' expect_line "Gate-family diff base: $C0 (merge-base of origin/main and HEAD)" expect_changed 'the feature edit only, not C1' "M docs/guide.md" @@ -549,7 +647,17 @@ expect_rc 0 expect_warnings '' '' expect_verdicts slot_lookup query_options_erasure comment_mask_corpus expect_reason pm_dispatch_gates 'no changed path is in its read-set' -expect_line 'Gate families: 3 run, 6 skipped' +expect_line 'Gate families: 3 run, 7 skipped' + +git_q -C "$REPO" checkout -q -B feature-19753 "$C0" +printf 'export const entry = 2;\n' > "$REPO/packages/spec/src/migrations/entries/semantic/17.x.ts" +git_q -C "$REPO" commit -q -am 'F3: an entry edited on a feature branch, registry not regenerated' +run_case 'pull_request: an entry edited on a feature branch runs the migration registry, decided against the merge base' "$REPO" pull_request main '' +expect_rc 0 +expect_warnings '' '' +expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus +expect_reason migration_registry '(M, workspace)' +expect_line "Gate-family diff base: $C0 (merge-base of origin/main and HEAD)" git_q -C "$REPO" checkout -q -B feature "$F1" run_case 'pull_request: no base branch in the payload' "$REPO" pull_request '' '' @@ -577,6 +685,7 @@ expect_rc 0 expect_warnings 'fetch + #6195' '::warning::Could not fetch origin/nope; the merge-base resolution below will decide. ::warning::Could not resolve merge-base(origin/nope, HEAD); every gate family runs rather than guessing which paths changed (#6195).' expect_all_run +expect_reason migration_registry 'merge-base(origin/nope, HEAD) unresolvable' # ── The YAML half of the contract ─────────────────────────────────────────── # Read from the real workflow: the selector step, the `if:` spellings, and @@ -640,10 +749,11 @@ pin_step self_test_workflow_commands 'node scripts/check-self-test-workflow-comm # that no workflow passes. pin_step verify_lock 'bash scripts/pm/os-verify-lock.sh' pin_step comment_mask_corpus 'node scripts/check-comment-mask-corpus.mjs' +pin_step migration_registry 'pnpm --filter @objectstack/spec check:migration-registry' # ── Verdict ───────────────────────────────────────────────────────────────── # #4690: a battery that ran nothing is a failure, never a pass. -if [ "$cases" -lt 42 ] || [ "$checks" -lt 220 ]; then +if [ "$cases" -lt 56 ] || [ "$checks" -lt 293 ]; then echo "SELFTEST FAILED: only $cases case(s) / $checks check(s) ran -- the battery is short" exit 1 fi diff --git a/scripts/ci/select-gate-families.sh b/scripts/ci/select-gate-families.sh index 0f26db27f88..e6c2f3c27b0 100644 --- a/scripts/ci/select-gate-families.sh +++ b/scripts/ci/select-gate-families.sh @@ -117,6 +117,18 @@ # as its population, reads the workflow tree and # .github/actions to learn which self-tests CI runs, # and spawns those self-tests. +# migration_registry `pnpm --filter @objectstack/spec +# check:migration-registry` (#19753): tsx runs +# packages/spec/scripts/build-migration-registry.ts, +# its own self-test and then its check. It reads the +# generated registry and the entry directories it +# concatenates -- both under +# packages/spec/src/migrations/ -- plus its own +# source, the package manifest pnpm resolves the +# script through, and the package tsconfig.json tsx +# loads from that directory (which extends the root +# one, and root configuration runs every family). +# Nothing else in the workspace, nothing outside it. # # Root configuration (`package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, # `turbo.json`, `tsconfig.json`, `eslint.config.mjs`, .gitignore, @@ -154,6 +166,17 @@ # behind the selector, or making one of these read-sets smaller -- is again a # maintainer call, taken here, under this script's self-test. # +# ## migration_registry ADDS a gate to the PR path; it scopes nothing away +# +# The other nine families were steps that ran on every PR before the selector +# existed. `check:migration-registry` ran in no CI step at all, so a PR that +# edited a migration entry without regenerating registry.ts shipped a stale, +# published registry with every required check green (#19753). The maintainer +# ruled it onto the required job as a scoped step, verbatim 「同意 A′」 +# (#19753): every PR is judged, and the step runs when a change touches the +# read-set declared above. Its skip is a RATCHET-grade claim -- no changed path +# is one the gate reads -- not the weaker tooling-self-test claim of #19498. +# # ## The interface # # OS_GATE_EVENT_NAME `github.event_name` @@ -172,7 +195,7 @@ set -euo pipefail # The family ids, in the order the job runs them. `--families` prints them so # the self-test can pin the workflow's `if:` set against this list without a # second transcription. -FAMILIES='slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock' +FAMILIES='migration_registry slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock' if [ "${1:-}" = '--families' ]; then for id in $FAMILIES; do echo "$id"; done @@ -473,6 +496,21 @@ family_reads() { *) return 0 ;; esac ;; + migration_registry) + # The read-set declared in the header, named path by path: the migration + # tree (the entries AND the generated registry -- an edit to either side + # alone is exactly the drift this gate reports), the generator, and the + # two package files that decide how it runs. Every other spec file skips. + case "$path" in + packages/spec/src/migrations/*) return 0 ;; + packages/spec/scripts/build-migration-registry.ts) return 0 ;; + packages/spec/package.json|packages/spec/tsconfig.json) return 0 ;; + esac + case "$class" in + docs|changeset|workflow|agent-config|scripts|verify-lock|workspace) return 1 ;; + *) return 0 ;; + esac + ;; *) return 0 ;; esac }