mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-08-25 08:32:14 +00:00
180 lines
7.9 KiB
YAML
180 lines
7.9 KiB
YAML
name: SONA Drift Protection
|
|
|
|
# Guards the three SONA implementations against behavioral drift
|
|
# (issues #519/#553 — the same learn-from-feedback stub shipped in both
|
|
# crates/sona and the ruvllm TS package). Three gates, in order:
|
|
# 1. stub-tripwire — static no-op seam detector (fast, fails early)
|
|
# 2. harness — behavioral parity across implementations
|
|
# 3. rvf-fingerprint — output fingerprint vs committed reference.rvf
|
|
# One Linux leg only — this is the drift gate, not a platform-parity matrix
|
|
# (sona-napi.yml owns cross-platform builds).
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'crates/sona/**'
|
|
- 'npm/packages/sona/**'
|
|
- 'npm/packages/ruvllm/src/sona.ts'
|
|
- 'npm/packages/ruvllm/src/lora.ts'
|
|
- 'npm/packages/ruvllm/src/types.ts'
|
|
- 'npm/packages/ruvector/src/core/intelligence-engine.ts'
|
|
- 'scripts/sona-drift/**'
|
|
- '.github/workflows/sona-drift.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'crates/sona/**'
|
|
- 'npm/packages/sona/**'
|
|
- 'npm/packages/ruvllm/src/sona.ts'
|
|
- 'npm/packages/ruvllm/src/lora.ts'
|
|
- 'npm/packages/ruvllm/src/types.ts'
|
|
- 'npm/packages/ruvector/src/core/intelligence-engine.ts'
|
|
- 'scripts/sona-drift/**'
|
|
- '.github/workflows/sona-drift.yml'
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
NO_COLOR: '1'
|
|
|
|
jobs:
|
|
drift-gate:
|
|
name: Drift gate (parity + fingerprint)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Setup Rust
|
|
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
|
|
|
|
- name: Cache Rust
|
|
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
|
|
with:
|
|
# crates/sona is a root-workspace member, so napi's
|
|
# --cargo-cwd build lands in the shared ./target dir.
|
|
key: sona-drift
|
|
|
|
- name: Cache npm
|
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
|
|
with:
|
|
path: ~/.npm
|
|
key: npm-sona-drift-${{ runner.os }}-${{ hashFiles('npm/packages/sona/package-lock.json', 'npm/packages/ruvllm/package.json') }}
|
|
restore-keys: |
|
|
npm-sona-drift-${{ runner.os }}-
|
|
|
|
# ── Gate 1: static no-op seam detector (cheapest — fail before building) ──
|
|
- name: Stub tripwire (static no-op seam detector)
|
|
run: node scripts/sona-drift/stub-tripwire.mjs
|
|
|
|
# ── Build the two artifacts the harness compares ──
|
|
- name: Build SONA NAPI binding (crates/sona via napi)
|
|
working-directory: npm/packages/sona
|
|
run: |
|
|
npm install --workspaces=false --no-audit --no-fund
|
|
npm run build
|
|
|
|
- name: Build ruvllm dist (TS implementation)
|
|
working-directory: npm/packages/ruvllm
|
|
run: |
|
|
npm install --workspaces=false --no-audit --no-fund
|
|
npm run build
|
|
|
|
# The RVF fingerprint store needs the @ruvector/rvf-node native backend,
|
|
# which nothing else on this runner installs (first CI run failed with
|
|
# BackendNotFound — that was infra, not drift).
|
|
- name: Install RVF node backend (fingerprint store)
|
|
working-directory: npm/packages/rvf
|
|
run: npm install --workspaces=false --no-audit --no-fund
|
|
|
|
# ── Gate 2: behavioral parity harness ──
|
|
- name: Behavioral parity harness
|
|
id: harness
|
|
run: |
|
|
set -o pipefail
|
|
HARNESS_EXIT=0
|
|
node scripts/sona-drift/harness.mjs --json | tee sona-drift-report.json || HARNESS_EXIT=$?
|
|
if [ "$HARNESS_EXIT" -ne 0 ]; then
|
|
# Best-effort: name the drifted implementation + contract from the
|
|
# JSON report (schema-defensive — falls back to a generic message).
|
|
SUMMARY=$(node -e '
|
|
try {
|
|
const r = JSON.parse(require("fs").readFileSync("sona-drift-report.json", "utf8"));
|
|
const items = [].concat(r.failures || r.contracts || r.results || []);
|
|
const bad = items.filter(c => c && (c.pass === false || c.ok === false || c.status === "fail" || c.drifted === true));
|
|
if (bad.length) {
|
|
console.log(bad.map(c =>
|
|
(c.implementation || c.impl || c.name || "unknown implementation") +
|
|
" drifted on " + (c.contract || c.test || c.id || "a contract")
|
|
).join("; "));
|
|
} else if (r.summary) {
|
|
console.log(typeof r.summary === "string" ? r.summary : JSON.stringify(r.summary));
|
|
}
|
|
} catch (e) { /* report unparsable — generic message below */ }
|
|
' || true)
|
|
echo "::error title=SONA behavioral drift::${SUMMARY:-Behavioral parity harness failed (see sona-drift-report artifact)} — the SONA implementations (crates/sona, npm/packages/ruvllm/src/sona.ts, npm/packages/sona, npm/packages/ruvector/src/core/intelligence-engine.ts) no longer agree. Fix the drifted implementation; if the behavior change is intentional: run node scripts/sona-drift/rvf-fingerprint.mjs --update and commit reference.rvf."
|
|
exit "$HARNESS_EXIT"
|
|
fi
|
|
|
|
- name: Upload parity report
|
|
if: always()
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
with:
|
|
name: sona-drift-report
|
|
path: sona-drift-report.json
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|
|
|
|
# ── Gate 3: RVF fingerprint vs committed reference ──
|
|
- name: RVF fingerprint validation
|
|
id: fingerprint
|
|
run: |
|
|
if ! node scripts/sona-drift/rvf-fingerprint.mjs; then
|
|
echo "::error title=SONA RVF fingerprint drift::Current SONA fingerprint diverged from committed scripts/sona-drift/reference.rvf — observable behavior of a SONA implementation changed. If intentional: run node scripts/sona-drift/rvf-fingerprint.mjs --update and commit reference.rvf."
|
|
# Regenerate the current fingerprint so it can be uploaded and
|
|
# diffed against the committed reference.
|
|
node scripts/sona-drift/rvf-fingerprint.mjs --update || true
|
|
cp scripts/sona-drift/reference.rvf sona-drift-current.rvf 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
|
|
- name: Upload current fingerprint (on drift)
|
|
if: failure() && steps.fingerprint.outcome == 'failure'
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
with:
|
|
name: sona-drift-current-fingerprint
|
|
path: sona-drift-current.rvf
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|
|
|
|
# ── Guard the guard: deleting the harness must fail CI, not disable it ──
|
|
# (same spirit as the artifact-presence guards in sona-napi.yml)
|
|
- name: Guard the guard — drift scripts and reference must exist
|
|
if: always()
|
|
run: |
|
|
fail=0
|
|
for f in scripts/sona-drift/harness.mjs \
|
|
scripts/sona-drift/rvf-fingerprint.mjs \
|
|
scripts/sona-drift/stub-tripwire.mjs; do
|
|
if [ ! -f "$f" ]; then
|
|
echo "::error file=$f::SONA drift-gate script is missing — the drift gate would be silently disabled. Restore it or remove this workflow deliberately."
|
|
fail=1
|
|
fi
|
|
done
|
|
if [ ! -s scripts/sona-drift/reference.rvf ]; then
|
|
echo "::error file=scripts/sona-drift/reference.rvf::reference.rvf is missing or empty — fingerprint validation has nothing to compare against. Run node scripts/sona-drift/rvf-fingerprint.mjs --update and commit it."
|
|
fail=1
|
|
fi
|
|
exit $fail
|