vecshift 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. vecshift-0.1.0/.github/workflows/ci.yml +146 -0
  2. vecshift-0.1.0/.github/workflows/release.yml +146 -0
  3. vecshift-0.1.0/.gitignore +33 -0
  4. vecshift-0.1.0/CHANGELOG.md +140 -0
  5. vecshift-0.1.0/Dockerfile +35 -0
  6. vecshift-0.1.0/LICENSE +202 -0
  7. vecshift-0.1.0/NOTICE +4 -0
  8. vecshift-0.1.0/PKG-INFO +264 -0
  9. vecshift-0.1.0/README.md +228 -0
  10. vecshift-0.1.0/RELEASING.md +75 -0
  11. vecshift-0.1.0/SECURITY.md +30 -0
  12. vecshift-0.1.0/demo/compose.yaml +42 -0
  13. vecshift-0.1.0/demo/out/.gitignore +2 -0
  14. vecshift-0.1.0/demo/run.sh +60 -0
  15. vecshift-0.1.0/demo/seed.py +254 -0
  16. vecshift-0.1.0/docs/architecture.md +183 -0
  17. vecshift-0.1.0/docs/bench.md +128 -0
  18. vecshift-0.1.0/docs/connectors/pgvector.md +120 -0
  19. vecshift-0.1.0/docs/demo.md +76 -0
  20. vecshift-0.1.0/docs/eval.md +129 -0
  21. vecshift-0.1.0/docs/images/doctor-report.png +0 -0
  22. vecshift-0.1.0/docs/images/eval-report.png +0 -0
  23. vecshift-0.1.0/docs/images/logo/vecshift-dark.svg +1 -0
  24. vecshift-0.1.0/docs/images/logo/vecshift-light.svg +1 -0
  25. vecshift-0.1.0/docs/images/logo/vecshift-mark-dark.svg +1 -0
  26. vecshift-0.1.0/docs/images/logo/vecshift-mark-light.svg +1 -0
  27. vecshift-0.1.0/docs/images/logo/vecshift-mark.svg +1 -0
  28. vecshift-0.1.0/docs/images/social-preview.png +0 -0
  29. vecshift-0.1.0/docs/migrations.md +328 -0
  30. vecshift-0.1.0/docs/prior-art.md +44 -0
  31. vecshift-0.1.0/docs/roadmap.md +125 -0
  32. vecshift-0.1.0/docs/security.md +116 -0
  33. vecshift-0.1.0/pyproject.toml +130 -0
  34. vecshift-0.1.0/scripts/release_notes.py +42 -0
  35. vecshift-0.1.0/src/vecshift/__init__.py +15 -0
  36. vecshift-0.1.0/src/vecshift/assets/bench.css +39 -0
  37. vecshift-0.1.0/src/vecshift/assets/eval.css +91 -0
  38. vecshift-0.1.0/src/vecshift/assets/report.css +252 -0
  39. vecshift-0.1.0/src/vecshift/assets/report.js +50 -0
  40. vecshift-0.1.0/src/vecshift/bench/__init__.py +18 -0
  41. vecshift-0.1.0/src/vecshift/bench/corpus.py +175 -0
  42. vecshift-0.1.0/src/vecshift/bench/generate.py +106 -0
  43. vecshift-0.1.0/src/vecshift/bench/html.py +325 -0
  44. vecshift-0.1.0/src/vecshift/bench/metrics.py +50 -0
  45. vecshift-0.1.0/src/vecshift/bench/runner.py +183 -0
  46. vecshift-0.1.0/src/vecshift/cli.py +236 -0
  47. vecshift-0.1.0/src/vecshift/cli_apply.py +283 -0
  48. vecshift-0.1.0/src/vecshift/cli_bench.py +354 -0
  49. vecshift-0.1.0/src/vecshift/cli_cutover.py +431 -0
  50. vecshift-0.1.0/src/vecshift/cli_eval.py +591 -0
  51. vecshift-0.1.0/src/vecshift/cli_plan.py +335 -0
  52. vecshift-0.1.0/src/vecshift/cli_style.py +57 -0
  53. vecshift-0.1.0/src/vecshift/connectors/__init__.py +1 -0
  54. vecshift-0.1.0/src/vecshift/connectors/pgvector/__init__.py +29 -0
  55. vecshift-0.1.0/src/vecshift/connectors/pgvector/connection.py +155 -0
  56. vecshift-0.1.0/src/vecshift/connectors/pgvector/documents.py +96 -0
  57. vecshift-0.1.0/src/vecshift/connectors/pgvector/inspect.py +427 -0
  58. vecshift-0.1.0/src/vecshift/connectors/pgvector/search.py +240 -0
  59. vecshift-0.1.0/src/vecshift/connectors/pgvector/switch.py +481 -0
  60. vecshift-0.1.0/src/vecshift/connectors/pgvector/target.py +195 -0
  61. vecshift-0.1.0/src/vecshift/connectors/pgvector/writer.py +431 -0
  62. vecshift-0.1.0/src/vecshift/core/__init__.py +4 -0
  63. vecshift-0.1.0/src/vecshift/core/capabilities.py +33 -0
  64. vecshift-0.1.0/src/vecshift/core/contracts.py +57 -0
  65. vecshift-0.1.0/src/vecshift/core/fingerprint.py +57 -0
  66. vecshift-0.1.0/src/vecshift/core/record.py +75 -0
  67. vecshift-0.1.0/src/vecshift/doctor/__init__.py +15 -0
  68. vecshift-0.1.0/src/vecshift/doctor/checks.py +490 -0
  69. vecshift-0.1.0/src/vecshift/doctor/findings.py +78 -0
  70. vecshift-0.1.0/src/vecshift/doctor/html.py +493 -0
  71. vecshift-0.1.0/src/vecshift/doctor/profile.py +69 -0
  72. vecshift-0.1.0/src/vecshift/embeddings/__init__.py +22 -0
  73. vecshift-0.1.0/src/vecshift/embeddings/cache.py +86 -0
  74. vecshift-0.1.0/src/vecshift/embeddings/providers.py +244 -0
  75. vecshift-0.1.0/src/vecshift/embeddings/spec.py +240 -0
  76. vecshift-0.1.0/src/vecshift/eval/__init__.py +20 -0
  77. vecshift-0.1.0/src/vecshift/eval/html.py +444 -0
  78. vecshift-0.1.0/src/vecshift/eval/metrics.py +81 -0
  79. vecshift-0.1.0/src/vecshift/eval/queries.py +97 -0
  80. vecshift-0.1.0/src/vecshift/eval/runner.py +394 -0
  81. vecshift-0.1.0/src/vecshift/html_kit.py +143 -0
  82. vecshift-0.1.0/src/vecshift/jobs/__init__.py +5 -0
  83. vecshift-0.1.0/src/vecshift/jobs/spec.py +202 -0
  84. vecshift-0.1.0/src/vecshift/migrate/__init__.py +6 -0
  85. vecshift-0.1.0/src/vecshift/migrate/engine.py +272 -0
  86. vecshift-0.1.0/src/vecshift/migrate/state.py +50 -0
  87. vecshift-0.1.0/src/vecshift/planning/__init__.py +14 -0
  88. vecshift-0.1.0/src/vecshift/planning/plan.py +87 -0
  89. vecshift-0.1.0/src/vecshift/planning/planner.py +493 -0
  90. vecshift-0.1.0/src/vecshift/py.typed +0 -0
  91. vecshift-0.1.0/tests/__init__.py +0 -0
  92. vecshift-0.1.0/tests/integration/__init__.py +0 -0
  93. vecshift-0.1.0/tests/integration/conftest.py +69 -0
  94. vecshift-0.1.0/tests/integration/test_pgvector_apply.py +340 -0
  95. vecshift-0.1.0/tests/integration/test_pgvector_bench.py +49 -0
  96. vecshift-0.1.0/tests/integration/test_pgvector_cutover.py +500 -0
  97. vecshift-0.1.0/tests/integration/test_pgvector_doctor.py +342 -0
  98. vecshift-0.1.0/tests/integration/test_pgvector_eval.py +280 -0
  99. vecshift-0.1.0/tests/integration/test_pgvector_plan.py +211 -0
  100. vecshift-0.1.0/tests/test_bench.py +361 -0
  101. vecshift-0.1.0/tests/test_cli.py +36 -0
  102. vecshift-0.1.0/tests/test_contracts.py +42 -0
  103. vecshift-0.1.0/tests/test_doctor_checks.py +169 -0
  104. vecshift-0.1.0/tests/test_doctor_html.py +136 -0
  105. vecshift-0.1.0/tests/test_embeddings.py +254 -0
  106. vecshift-0.1.0/tests/test_eval.py +273 -0
  107. vecshift-0.1.0/tests/test_eval_html.py +83 -0
  108. vecshift-0.1.0/tests/test_fingerprint.py +56 -0
  109. vecshift-0.1.0/tests/test_jobs.py +59 -0
  110. vecshift-0.1.0/tests/test_migrate.py +304 -0
  111. vecshift-0.1.0/tests/test_pgvector_connection.py +83 -0
  112. vecshift-0.1.0/tests/test_planner.py +203 -0
  113. vecshift-0.1.0/tests/test_record.py +47 -0
  114. vecshift-0.1.0/tests/test_release.py +47 -0
  115. vecshift-0.1.0/tests/test_security.py +352 -0
@@ -0,0 +1,146 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ schedule:
8
+ # Weekly, so newly published vulnerabilities in dependencies are caught between commits.
9
+ - cron: "17 6 * * 1"
10
+
11
+ # Least privilege: jobs can read the repository and nothing else.
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.ref }}
17
+ cancel-in-progress: true
18
+
19
+ # Actions are pinned to full commit SHAs so a moved or compromised tag can't change what
20
+ # runs here. Dependabot keeps the pins (and the version comments) up to date.
21
+
22
+ jobs:
23
+ lint:
24
+ name: Lint and type check
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 10
27
+ steps:
28
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
29
+ with:
30
+ persist-credentials: false
31
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
32
+ - run: uv sync --locked
33
+ - run: uv run ruff check .
34
+ - run: uv run ruff format --check .
35
+ - run: uv run mypy
36
+
37
+ test:
38
+ name: Test (Python ${{ matrix.python-version }})
39
+ runs-on: ubuntu-latest
40
+ timeout-minutes: 15
41
+ strategy:
42
+ fail-fast: false
43
+ matrix:
44
+ python-version: ["3.11", "3.12", "3.13"]
45
+ env:
46
+ UV_PYTHON: ${{ matrix.python-version }}
47
+ steps:
48
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
49
+ with:
50
+ persist-credentials: false
51
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
52
+ - run: uv sync --locked
53
+ - run: uv run pytest --cov=vecshift --cov-report=term-missing
54
+
55
+ integration:
56
+ name: Integration (PostgreSQL ${{ matrix.postgres }} + pgvector)
57
+ runs-on: ubuntu-latest
58
+ timeout-minutes: 15
59
+ strategy:
60
+ fail-fast: false
61
+ matrix:
62
+ postgres: ["16", "17"]
63
+ services:
64
+ postgres:
65
+ # Docker Hub's mirror on Google: the same images (identical digests), without Docker
66
+ # Hub's rate limit on anonymous pulls from CI runners.
67
+ image: mirror.gcr.io/pgvector/pgvector:pg${{ matrix.postgres }}
68
+ env:
69
+ # A throwaway database that exists only for this job.
70
+ POSTGRES_PASSWORD: postgres
71
+ ports:
72
+ - 5432:5432
73
+ options: >-
74
+ --health-cmd "pg_isready -U postgres"
75
+ --health-interval 5s
76
+ --health-timeout 5s
77
+ --health-retries 10
78
+ env:
79
+ VECSHIFT_TEST_PG_DSN: postgresql://postgres:postgres@localhost:5432/postgres
80
+ steps:
81
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
82
+ with:
83
+ persist-credentials: false
84
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
85
+ - run: uv sync --locked
86
+ - run: uv run pytest tests/integration
87
+
88
+ security:
89
+ name: Security audit
90
+ runs-on: ubuntu-latest
91
+ timeout-minutes: 10
92
+ steps:
93
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
94
+ with:
95
+ persist-credentials: false
96
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
97
+ - name: Known vulnerabilities in locked dependencies
98
+ run: |
99
+ uv export --frozen --all-extras --all-groups --no-hashes --no-emit-project -o requirements-audit.txt
100
+ uvx pip-audit==2.10.1 --strict -r requirements-audit.txt
101
+ - name: GitHub Actions workflow security
102
+ run: uvx zizmor==1.30.1 --offline .github/workflows/
103
+
104
+ package:
105
+ name: Package
106
+ runs-on: ubuntu-latest
107
+ timeout-minutes: 10
108
+ steps:
109
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
110
+ with:
111
+ persist-credentials: false
112
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
113
+ - run: uv build
114
+ - name: PyPI metadata and README render
115
+ run: uvx twine==7.0.0 check --strict dist/*
116
+ - name: The wheel installs and runs on its own
117
+ run: |
118
+ uv venv /tmp/wheel-test
119
+ uv pip install --python /tmp/wheel-test/bin/python dist/*.whl
120
+ /tmp/wheel-test/bin/vecshift --version
121
+ /tmp/wheel-test/bin/python -c "from importlib.resources import files; assert files('vecshift').joinpath('assets/report.js').is_file()"
122
+
123
+ demo:
124
+ name: Docker image and demo
125
+ runs-on: ubuntu-latest
126
+ timeout-minutes: 15
127
+ env:
128
+ # Docker Hub's images through Google's mirror (same digests), to avoid rate limits.
129
+ PYTHON_IMAGE: mirror.gcr.io/library/python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f
130
+ PGVECTOR_IMAGE: mirror.gcr.io/pgvector/pgvector:pg17@sha256:ac08538c6f8b9904c33c8224c5e5706dbe760aca29db1d096972b4052c22a75d
131
+ steps:
132
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
133
+ with:
134
+ persist-credentials: false
135
+ - name: Run the offline demo end to end
136
+ working-directory: demo
137
+ run: |
138
+ chmod a+w out
139
+ docker compose up --build --abort-on-container-exit --exit-code-from vecshift
140
+ test -s out/eval.html
141
+ - name: Roll back
142
+ working-directory: demo
143
+ run: docker compose run --rm --entrypoint vecshift vecshift rollback --yes
144
+ - if: always()
145
+ working-directory: demo
146
+ run: docker compose down --volumes
@@ -0,0 +1,146 @@
1
+ name: Release
2
+
3
+ # Pushing a tag such as v0.1.0 publishes that version: the package to PyPI, the image to
4
+ # GitHub Container Registry, and a GitHub release with the CHANGELOG section as notes.
5
+ # See RELEASING.md.
6
+
7
+ on:
8
+ push:
9
+ tags: ["v[0-9]+.[0-9]+.[0-9]+*"]
10
+
11
+ # No permissions by default; each job asks for only what it needs.
12
+ permissions: {}
13
+
14
+ concurrency:
15
+ group: release
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ build:
20
+ name: Check and build
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 15
23
+ permissions:
24
+ contents: read
25
+ steps:
26
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
27
+ with:
28
+ persist-credentials: false
29
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
30
+ with:
31
+ # A release never restores a cache that another workflow could have written.
32
+ enable-cache: false
33
+ - run: uv sync --locked
34
+ - run: uv run ruff check .
35
+ - run: uv run ruff format --check .
36
+ - run: uv run mypy
37
+ - run: uv run pytest
38
+ - run: uv build
39
+ - name: The tag matches the package version
40
+ run: |
41
+ version="${GITHUB_REF_NAME#v}"
42
+ if [ ! -f "dist/vecshift-${version}-py3-none-any.whl" ]; then
43
+ echo "::error::Tag ${GITHUB_REF_NAME} doesn't match the version in src/vecshift/__init__.py:"
44
+ ls dist
45
+ exit 1
46
+ fi
47
+ - name: CHANGELOG.md has notes for this version
48
+ run: python scripts/release_notes.py "$GITHUB_REF_NAME" > /dev/null
49
+ - run: uvx twine==7.0.0 check --strict dist/*
50
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+ if-no-files-found: error
55
+
56
+ pypi:
57
+ name: Publish to PyPI
58
+ needs: build
59
+ runs-on: ubuntu-latest
60
+ timeout-minutes: 10
61
+ # Trusted publishing: PyPI accepts this job's OpenID Connect token, so no API token is
62
+ # stored anywhere. The environment can require a maintainer's approval.
63
+ environment:
64
+ name: pypi
65
+ url: https://pypi.org/project/vecshift/
66
+ permissions:
67
+ id-token: write # trusted publishing to PyPI
68
+ steps:
69
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
70
+ with:
71
+ name: dist
72
+ path: dist/
73
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
74
+
75
+ image:
76
+ name: Publish the Docker image
77
+ needs: build
78
+ runs-on: ubuntu-latest
79
+ timeout-minutes: 30
80
+ permissions:
81
+ contents: read
82
+ packages: write # push the image to ghcr.io
83
+ id-token: write # sign the provenance attestation
84
+ attestations: write # store it
85
+ steps:
86
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
87
+ with:
88
+ persist-credentials: false
89
+ - uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
90
+ - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
91
+ - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
92
+ with:
93
+ registry: ghcr.io
94
+ username: ${{ github.actor }}
95
+ password: ${{ secrets.GITHUB_TOKEN }}
96
+ - id: meta
97
+ uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
98
+ with:
99
+ images: ghcr.io/${{ github.repository }}
100
+ tags: |
101
+ type=semver,pattern={{version}}
102
+ type=semver,pattern={{major}}.{{minor}}
103
+ - id: push
104
+ uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
105
+ with:
106
+ context: .
107
+ # Docker Hub's base image through Google's mirror (same digest), to avoid rate limits.
108
+ build-args: |
109
+ PYTHON_IMAGE=mirror.gcr.io/library/python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f
110
+ platforms: linux/amd64,linux/arm64
111
+ push: true
112
+ tags: ${{ steps.meta.outputs.tags }}
113
+ labels: ${{ steps.meta.outputs.labels }}
114
+ provenance: mode=max
115
+ sbom: true
116
+ - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
117
+ with:
118
+ subject-name: ghcr.io/${{ github.repository }}
119
+ subject-digest: ${{ steps.push.outputs.digest }}
120
+ push-to-registry: true
121
+
122
+ github:
123
+ name: GitHub release
124
+ needs: [pypi, image]
125
+ runs-on: ubuntu-latest
126
+ timeout-minutes: 10
127
+ permissions:
128
+ contents: write # create the release
129
+ steps:
130
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
131
+ with:
132
+ persist-credentials: false
133
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
134
+ with:
135
+ name: dist
136
+ path: dist/
137
+ - name: Create the release
138
+ env:
139
+ GH_TOKEN: ${{ github.token }}
140
+ run: |
141
+ python scripts/release_notes.py "$GITHUB_REF_NAME" > notes.md
142
+ prerelease=""
143
+ case "$GITHUB_REF_NAME" in *a*|*b*|*rc*) prerelease="--prerelease" ;; esac
144
+ gh release create "$GITHUB_REF_NAME" dist/* --verify-tag \
145
+ --repo "$GITHUB_REPOSITORY" --title "vecshift ${GITHUB_REF_NAME#v}" \
146
+ --notes-file notes.md $prerelease
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+
8
+ # Virtual environments
9
+ .venv/
10
+ venv/
11
+
12
+ # Tooling caches
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ coverage.xml
18
+ htmlcov/
19
+
20
+ # VecShift local state
21
+ .vecshift/
22
+ *.dlq.jsonl
23
+
24
+ # Environment and secrets
25
+ .env
26
+ .env.*
27
+ !.env.example
28
+
29
+ # Editors and OS
30
+ .idea/
31
+ .vscode/
32
+ .DS_Store
33
+ requirements-audit.txt
@@ -0,0 +1,140 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
6
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0]
11
+
12
+ The first release.
13
+
14
+ ### Security
15
+
16
+ - API keys are never sent over unencrypted `http://` to remote hosts, redirects are never
17
+ followed, and credentials inside model URLs are rejected. URL query strings are hidden
18
+ wherever a spec is shown.
19
+ - `compat` servers only receive an API key when the spec names one with `key_env=`.
20
+ Previously `VECSHIFT_API_KEY` was sent to any compat URL.
21
+ - The embedding cache is now readable by its owner only (directory `0700`, file `0600`).
22
+ - HTML reports carry a strict Content Security Policy: no network access, and only their
23
+ own script, pinned by hash, may run.
24
+ - SQL generated for `apply` quotes identifiers using the server's keyword list.
25
+ - Crash tracebacks never print local variables, and `--dsn` with a password on the
26
+ command line prints a warning.
27
+ - CI runs `pip-audit`, `zizmor`, and Ruff's security rules on every change and weekly.
28
+ Actions are pinned to commit SHAs, and checkout no longer persists credentials.
29
+
30
+ ### Fixed
31
+
32
+ - On tables with constant writes, `apply` never finished (a few rows always changed during
33
+ each catch-up pass, so it stopped without building the index) and `cutover` could never
34
+ pass its final check. `apply` now finishes once at most 0.1% of rows (at least 100) are
35
+ left, counting them after the index build; each pass covers only what was pending when it
36
+ began; and `cutover` embeds up to 500 late rows while it holds the write lock.
37
+ - `eval` treated a busy table's few waiting rows as a partial migration and skipped latency.
38
+ - `eval`'s index recall now counts ties: a row exactly as close as the exact 10th result is
39
+ a correct answer. Tables with near-duplicate rows read far too low before.
40
+ - Schema changes wait at most 2 seconds (was 5) for a lock, which bounds how long
41
+ application writes can queue behind them.
42
+ - `apply` explains `could not resize shared memory segment` during the index build
43
+ (Docker's default 64 MB `/dev/shm`), and `plan`'s memory hint mentions `--shm-size`.
44
+ - `plan` described cutover with the wrong column name (`_previous`; it's `_old`).
45
+
46
+ - `plan` (and so `apply`) and `cutover --check` refuse a table whose row-level security binds
47
+ the connecting role, such as one with `FORCE ROW LEVEL SECURITY`. `apply` used to report
48
+ success while skipping the rows the role couldn't see.
49
+ - They also refuse partitioned tables, where `apply` embedded every row and then failed to
50
+ build the index, since PostgreSQL can't build one concurrently on a partitioned table.
51
+
52
+ - `vecshift doctor` sampled only the start of large tables, so rows written later (often by a
53
+ newer model) could be missed. Samples are now spread across the table.
54
+
55
+ ### Added
56
+
57
+ - A Docker image that runs as a non-root user, and an offline demo:
58
+ `cd demo && docker compose up` runs doctor, plan, apply, eval, and cutover on a sample
59
+ table of English and Arabic articles, with no API key. See `docs/demo.md`.
60
+ - Job files accept the built-in `hash/N` models, for trying vecshift out; `plan` warns
61
+ that they're test models (`plan.baseline_model`).
62
+ - `vecshift eval` compares the old and new vectors on your data before cutover, without
63
+ re-embedding documents. It reports recall@1, recall@10, and MRR@10 overall and for each
64
+ query → document script pair (Arabic and Latin), how much the top results changed,
65
+ query embedding latency, and search latency (p50, p95, p99) with index recall across
66
+ `hnsw.ef_search` or `ivfflat.probes` settings. It ends with GO, NO-GO, or INCONCLUSIVE
67
+ (exit status 0, 1, or 3), with optional latency limits. If the old model is unavailable
68
+ it judges the old side from stored vectors, by how well each row's nearest rows stay in
69
+ the same document. Queries come from the rows themselves, a labeled file, or an LLM,
70
+ optionally in the other language (`--cross-language`). It's read-only, and `--dsn-env`
71
+ can point it at a replica.
72
+ - `vecshift eval --html FILE` writes a self-contained report: the verdict, headline numbers
73
+ with their change, recall@10 per language as a before → after chart, and a latency vs
74
+ accuracy chart for both sides, each with a table view, in light and dark themes.
75
+ - `source.model` in the job file names the model that made the current vectors.
76
+
77
+ - `vecshift cutover` switches searches to the new vectors: in one transaction it renames
78
+ the live column to `<name>_old` and the new column to `<name>`, so the application's SQL
79
+ doesn't change. It first embeds rows added or edited since the last `apply`, then
80
+ confirms under a brief write lock that every row has a new vector. `--check` reports
81
+ whether the switch is safe without changing anything: missing vectors, an unbuilt index,
82
+ views bound to the column, and a vector size change the application must match.
83
+ - `vecshift rollback` renames the columns back, and reports rows added or edited since
84
+ cutover that have no old-model vector.
85
+ - `vecshift cleanup` drops the old column, its index, and its trigger once you're sure.
86
+ - `vecshift apply --until PERCENT` stops once that share of rows has a new vector.
87
+
88
+ ### Changed
89
+
90
+ - The sync trigger keeps a new vector the application writes itself in the same update,
91
+ and pins its function's `search_path`. Dropping the column it guards by hand is refused
92
+ instead of leaving a broken trigger behind.
93
+ - `plan` reports a table that was already cut over and not cleaned up.
94
+
95
+ - `vecshift apply` runs a migration on pgvector and Supabase: it adds the new column and a
96
+ trigger that clears a row's new vector when its text changes, embeds every row in
97
+ resumable batches, makes catch-up passes for rows edited during the run, then builds the
98
+ index concurrently and checks every vector's size. Writes are guarded so a vector only
99
+ lands if the row's text is unchanged. It stops cleanly on Ctrl-C or before passing
100
+ `limits.budget_usd` (counted across runs), isolates rows the provider rejects, and
101
+ `--json` streams progress as JSON lines. Exit status 3 means "stopped; run again".
102
+ - `plan` now lists the sync trigger among the changes, and reports tables with a
103
+ composite primary key, which `apply` doesn't support yet.
104
+
105
+ - `vecshift init` writes a commented `vecshift.yaml` job file describing a migration: the
106
+ source table, the side-by-side target column, the new model, and limits such as a budget.
107
+ - `vecshift plan` checks a job against the database without changing anything. It shows
108
+ the SQL apply would run, estimates rows, tokens, cost, duration, storage, and index build
109
+ memory, and reports anything that would make the migration fail, exiting with status 1 on
110
+ errors. `--probe` measures the real model's size, token counts, and speed on 16 rows.
111
+ - `vecshift bench` compares embedding models on a sample of your documents, from a JSONL
112
+ file or a pgvector table. It reports recall@1, recall@10, MRR@10, query latency,
113
+ throughput, cost per million documents, and storage, as a terminal table, `--json`, or an
114
+ `--html` leaderboard. Queries come from the documents themselves (free), from an LLM
115
+ (`--generate-queries`), or from your labeled file (`--queries`).
116
+ - Embedding providers: OpenAI, Ollama, any OpenAI-compatible server, and a free hashing
117
+ baseline, with batching, retries that respect rate limits, known query and document
118
+ prefixes, and an on-disk embedding cache. Installed with `pip install 'vecshift[bench]'`.
119
+ - `bench` shows estimated tokens and cost and asks before sending text to a remote API.
120
+ - Project logo, in light and dark versions, with a GitHub social preview image. The HTML
121
+ report shows it in its header and as its browser-tab icon. `scripts/make_logo.py`
122
+ regenerates the logo files.
123
+ - `vecshift doctor --html FILE` writes a self-contained HTML report: a verdict, key numbers,
124
+ filterable findings, and charts of vector lengths, models, and vector sizes. It works
125
+ offline, supports light and dark themes, and contains statistics only.
126
+ - The JSON report gains a `facts` section with the measurements behind the findings.
127
+
128
+ - `vecshift doctor` for pgvector, including Supabase: a read-only check of an existing index
129
+ for missing source text, mixed vector sizes or models, zero and unnormalized vectors,
130
+ duplicates, missing ANN indexes, row-level security gaps, and how live writes could be
131
+ tracked during a migration. Supports `--json` and `--fail-on` for CI.
132
+ - PostgreSQL connections that work with every Supabase mode (direct, session pooler,
133
+ transaction pooler), require TLS for Supabase hosts, and never print passwords.
134
+ - Canonical `Record` type with tombstones and `updated_at` conflict resolution.
135
+ - `EmbeddingFingerprint` and model tags that identify a vector space.
136
+ - `Capability` flags and plugin contracts for sources, targets, and embedding providers.
137
+ - `vecshift fingerprint` and `vecshift --version` commands.
138
+
139
+ [Unreleased]: https://github.com/Osamamu64/vecshift/compare/v0.1.0...HEAD
140
+ [0.1.0]: https://github.com/Osamamu64/vecshift/releases/tag/v0.1.0
@@ -0,0 +1,35 @@
1
+ # syntax=docker/dockerfile:1
2
+ # The vecshift CLI in a small image that runs as a non-root user.
3
+ #
4
+ # docker build -t vecshift .
5
+ # docker run --rm -e VECSHIFT_DSN vecshift doctor --table documents
6
+ #
7
+ # The base image is pinned by digest; Dependabot keeps it current. PYTHON_IMAGE can point
8
+ # at a mirror with the same digest, such as mirror.gcr.io/library/python.
9
+ ARG PYTHON_IMAGE=python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f
10
+
11
+ FROM ${PYTHON_IMAGE} AS build
12
+ RUN pip install --no-cache-dir uv==0.11.32
13
+ WORKDIR /src
14
+ COPY pyproject.toml uv.lock README.md LICENSE NOTICE ./
15
+ COPY src ./src
16
+ # Dependencies come from the lock file, checked against its hashes.
17
+ RUN uv export --frozen --no-dev --no-emit-project -o /dist/requirements.txt \
18
+ && uv build --wheel -o /dist
19
+
20
+ FROM ${PYTHON_IMAGE}
21
+ LABEL org.opencontainers.image.title="vecshift" \
22
+ org.opencontainers.image.description="Zero-downtime embedding model migrations for pgvector and Supabase" \
23
+ org.opencontainers.image.source="https://github.com/Osamamu64/vecshift" \
24
+ org.opencontainers.image.licenses="Apache-2.0"
25
+ ENV PYTHONDONTWRITEBYTECODE=1 \
26
+ PYTHONUNBUFFERED=1 \
27
+ PIP_DISABLE_PIP_VERSION_CHECK=1
28
+ RUN --mount=type=bind,from=build,source=/dist,target=/dist \
29
+ pip install --no-cache-dir --require-hashes -r /dist/requirements.txt \
30
+ && pip install --no-cache-dir --no-deps /dist/*.whl \
31
+ && useradd --create-home --uid 1000 --user-group vecshift
32
+ USER vecshift
33
+ WORKDIR /home/vecshift
34
+ ENTRYPOINT ["vecshift"]
35
+ CMD ["--help"]