From babc5f469049c15179e33a0a3e19fe132d98d19c Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 30 May 2026 01:16:08 +0700 Subject: [PATCH] ci: gate shared-contract drift for PluginKit and DatabaseType with a burn-down baseline --- .github/duplicate-contract-baseline.txt | 59 +++++++++++++++++++++++ .github/workflows/contract-drift.yml | 39 +++++++++++++++ scripts/audit-refactor-health.sh | 63 ++++++++++++++++++++----- 3 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 .github/duplicate-contract-baseline.txt create mode 100644 .github/workflows/contract-drift.yml diff --git a/.github/duplicate-contract-baseline.txt b/.github/duplicate-contract-baseline.txt new file mode 100644 index 000000000..c740495ae --- /dev/null +++ b/.github/duplicate-contract-baseline.txt @@ -0,0 +1,59 @@ +# Known divergent shared contracts pending Phase 1 consolidation (R-001 / R-002). +# scripts/audit-refactor-health.sh --check fails when a shared contract diverges +# that is NOT listed here. Burn this list down as contracts move into TableProCore; +# never add a fresh entry just to make new divergence pass. +# +# Format: : +# pluginkit: +# databasetype: + +databasetype:TablePro/Models/Connection/DatabaseConnection.swift + +pluginkit:ArrayExtension.swift +pluginkit:CompletionEntry.swift +pluginkit:ConnectionField.swift +pluginkit:ConnectionMode.swift +pluginkit:DocumentInspectorPlugin.swift +pluginkit:DriverConnectionConfig.swift +pluginkit:DriverPlugin.swift +pluginkit:EditorLanguage.swift +pluginkit:EnumValueParser.swift +pluginkit:ExportFormatPlugin.swift +pluginkit:GroupingStrategy.swift +pluginkit:HttpQueryTimeout.swift +pluginkit:HttpQueryTimeoutBox.swift +pluginkit:HugeIntFormatter.swift +pluginkit:ImportFormatPlugin.swift +pluginkit:Info.plist +pluginkit:MongoShellParser.swift +pluginkit:NavigationModel.swift +pluginkit:PathFieldRole.swift +pluginkit:PluginCapabilities.swift +pluginkit:PluginCapability.swift +pluginkit:PluginCellValue.swift +pluginkit:PluginColumnInfo.swift +pluginkit:PluginCreateDatabaseFormSpec.swift +pluginkit:PluginDatabaseDriver.swift +pluginkit:PluginDefaultSortProvider.swift +pluginkit:PluginDiagnostic.swift +pluginkit:PluginDriverError.swift +pluginkit:PluginExportDataSource.swift +pluginkit:PluginExportProgress.swift +pluginkit:PluginExportTypes.swift +pluginkit:PluginExportUtilities.swift +pluginkit:PluginImportDataSink.swift +pluginkit:PluginImportProgress.swift +pluginkit:PluginImportSource.swift +pluginkit:PluginImportTypes.swift +pluginkit:PluginPagedResult.swift +pluginkit:PluginProcedureFunctionSupport.swift +pluginkit:PluginQueryResult.swift +pluginkit:PluginSettingsStorage.swift +pluginkit:PluginStreamTypes.swift +pluginkit:PluginTableInfo.swift +pluginkit:PluginTableMetadata.swift +pluginkit:SQLDialectDescriptor.swift +pluginkit:SSLConfiguration.swift +pluginkit:SSLHandshakeError.swift +pluginkit:SchemaTypes.swift +pluginkit:SettablePlugin.swift diff --git a/.github/workflows/contract-drift.yml b/.github/workflows/contract-drift.yml new file mode 100644 index 000000000..fd23ccb57 --- /dev/null +++ b/.github/workflows/contract-drift.yml @@ -0,0 +1,39 @@ +name: Contract Drift + +on: + pull_request: + paths: + - "Plugins/TableProPluginKit/**" + - "Packages/TableProCore/Sources/TableProPluginKit/**" + - "Packages/TableProCore/Sources/TableProModels/**" + - "TablePro/Models/**" + - "TableProMobile/**" + - "scripts/audit-refactor-health.sh" + - ".github/duplicate-contract-baseline.txt" + - ".github/workflows/contract-drift.yml" + push: + branches: [main] + paths: + - "Plugins/TableProPluginKit/**" + - "Packages/TableProCore/Sources/TableProPluginKit/**" + - "Packages/TableProCore/Sources/TableProModels/**" + - "TablePro/Models/**" + - "TableProMobile/**" + - "scripts/audit-refactor-health.sh" + - ".github/duplicate-contract-baseline.txt" + - ".github/workflows/contract-drift.yml" + workflow_dispatch: + +concurrency: + group: contract-drift-${{ github.ref }} + cancel-in-progress: true + +jobs: + drift: + name: Shared contract drift + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Fail on new duplicate-contract drift + run: scripts/audit-refactor-health.sh --check diff --git a/scripts/audit-refactor-health.sh b/scripts/audit-refactor-health.sh index e692bea0f..b4a14596f 100755 --- a/scripts/audit-refactor-health.sh +++ b/scripts/audit-refactor-health.sh @@ -64,6 +64,30 @@ count_swift_matches() { grep_swift "$pattern" "$@" | grep -c . || true } +BASELINE_FILE=".github/duplicate-contract-baseline.txt" +PLUGINKIT_A="Plugins/TableProPluginKit" +PLUGINKIT_B="Packages/TableProCore/Sources/TableProPluginKit" +DATABASETYPE_AUTHORITATIVE="Packages/TableProCore/Sources/TableProModels/DatabaseType.swift" + +baseline_keys() { + [ -f "$BASELINE_FILE" ] || return 0 + sed -E 's/#.*//' "$BASELINE_FILE" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' | grep -v '^$' || true +} + +pluginkit_divergent_paths() { + [ -d "$PLUGINKIT_A" ] && [ -d "$PLUGINKIT_B" ] || return 0 + { diff -qr "$PLUGINKIT_A" "$PLUGINKIT_B" 2>/dev/null || true; } | sed -E \ + -e "s#^Files $PLUGINKIT_A/(.*) and .* differ#\\1#" \ + -e "s#^Only in $PLUGINKIT_A(/?[^:]*): #\\1/#" \ + -e "s#^Only in $PLUGINKIT_B(/?[^:]*): #\\1/#" \ + | sed -E 's#^/##' | sort -u | grep -v '^$' || true +} + +databasetype_extra_defs() { + grep_swift '^(public )?(struct|enum) DatabaseType[ :<]' TablePro Plugins Packages TableProMobile \ + | awk -F: '{print $1}' | sort -u | grep -vxF "$DATABASETYPE_AUTHORITATIVE" || true +} + report_loc_by_area() { section "Swift LOC by area" echo "Swift files (app + plugins + packages + mobile): $(count_swift_files TablePro Plugins Packages TableProMobile)" @@ -76,23 +100,20 @@ report_duplicate_contracts() { section "Duplicate shared contracts (R-001 / R-002 / R-008)" echo "DatabaseType definitions:" - local dbtype_defs dbtype_count + local dbtype_defs dbtype_defs=$(grep_swift '^(public )?(struct|enum) DatabaseType[ :<]' TablePro Plugins Packages TableProMobile | awk -F: '{print $1}' | sort -u) if [ -n "$dbtype_defs" ]; then printf '%s\n' "$dbtype_defs" | sed 's/^/ /' else echo " (none found)" fi - dbtype_count=$(printf '%s\n' "$dbtype_defs" | grep -c . || true) echo echo "PluginKit source trees:" - if [ -d "Plugins/TableProPluginKit" ] && [ -d "Packages/TableProCore/Sources/TableProPluginKit" ]; then - echo " both present (duplicate authoritative PluginKit)" - if $CHECK_MODE; then - echo " ⚠️ drift gate: duplicate PluginKit source trees still present" - DRIFT_FAILURES=$((DRIFT_FAILURES + 1)) - fi + local pk_divergent + pk_divergent=$(pluginkit_divergent_paths) + if [ -d "$PLUGINKIT_A" ] && [ -d "$PLUGINKIT_B" ]; then + echo " both present; $(printf '%s\n' "$pk_divergent" | grep -c . || true) divergent file(s) pending Phase 1 consolidation" else echo " single source ✅" fi @@ -105,9 +126,29 @@ report_duplicate_contracts() { echo " single source ✅" fi - if $CHECK_MODE && [ "${dbtype_count:-0}" -gt 1 ]; then - echo " ⚠️ drift gate: more than one production DatabaseType definition" - DRIFT_FAILURES=$((DRIFT_FAILURES + 1)) + if $CHECK_MODE; then + local baseline new_drift=0 path + baseline=$(baseline_keys) + while IFS= read -r path; do + [ -z "$path" ] && continue + if ! printf '%s\n' "$baseline" | grep -qxF "pluginkit:$path"; then + echo " ❌ new PluginKit divergence not in baseline: $path" + new_drift=$((new_drift + 1)) + fi + done <<< "$pk_divergent" + while IFS= read -r path; do + [ -z "$path" ] && continue + if ! printf '%s\n' "$baseline" | grep -qxF "databasetype:$path"; then + echo " ❌ DatabaseType defined outside the authoritative source and baseline: $path" + new_drift=$((new_drift + 1)) + fi + done <<< "$(databasetype_extra_defs)" + if [ "$new_drift" -gt 0 ]; then + DRIFT_FAILURES=$((DRIFT_FAILURES + new_drift)) + else + echo + echo " ✅ no new shared-contract drift beyond $BASELINE_FILE" + fi fi }