Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/actions/setup-node-yarn/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: "Setup Node + Yarn"
description: "Enable Corepack, set up Node.js with the Yarn 4 cache, and (optionally) install dependencies."

inputs:
node-version:
description: "Node.js version to install (mutually exclusive with node-version-file)."
required: false
default: ""
node-version-file:
description: "Path to a file holding the Node.js version, e.g. .nvmrc (mutually exclusive with node-version)."
required: false
default: ""
registry-url:
description: "Optional npm registry URL to configure for auth/publish."
required: false
default: ""
cache:
description: 'setup-node package-manager cache: "yarn"/"npm"/"pnpm", or "" to disable (e.g. a sparse checkout with no lockfile).'
required: false
default: "yarn"
install:
description: 'Run "yarn install --immutable". Set to "false" to only set up the toolchain.'
required: false
default: "true"

runs:
using: "composite"
steps:
- name: Validate inputs
if: ${{ inputs.node-version != '' && inputs.node-version-file != '' }}
shell: bash
run: |
echo "::error::setup-node-yarn: provide either 'node-version' or 'node-version-file', not both."
exit 1

# Corepack must be enabled BEFORE setup-node so its `cache: yarn` detection
# resolves to the packageManager-pinned Yarn 4 (not the runner's default Yarn 1).
- name: Enable Corepack
shell: bash
run: corepack enable

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
node-version-file: ${{ inputs.node-version-file }}
registry-url: ${{ inputs.registry-url }}
cache: ${{ inputs.cache }}

- name: Install dependencies
if: ${{ inputs.install == 'true' }}
shell: bash
run: yarn install --immutable
8 changes: 2 additions & 6 deletions .github/workflows/create-release-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,12 @@ jobs:
fetch-depth: 0
ref: ${{ inputs.base_branch }}

- name: Setup Node
uses: actions/setup-node@v6
- name: Setup Node + Yarn
uses: ./.github/actions/setup-node-yarn
with:
node-version-file: ".nvmrc"
cache: "yarn"
registry-url: "https://registry.npmjs.org/"

- name: Install deps
run: yarn install --frozen-lockfile

- name: Configure git user
run: |
git config user.name "github-actions[bot]"
Expand Down
8 changes: 2 additions & 6 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,15 @@ jobs:
echo "Branch: $BRANCH"
echo "Release line: $RELEASE_LINE"

- name: Setup Node
uses: actions/setup-node@v6
- name: Setup Node + Yarn
uses: ./.github/actions/setup-node-yarn
with:
node-version-file: ".nvmrc"
cache: "yarn"
registry-url: "https://registry.npmjs.org/"

- name: Update npm CLI for trusted publishing
run: npm install -g npm@latest

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Compute version
id: version
shell: bash
Expand Down
15 changes: 6 additions & 9 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,10 @@ jobs:
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v6
- name: Setup Node + Yarn
uses: ./.github/actions/setup-node-yarn
with:
node-version-file: ".nvmrc"
cache: "yarn"

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Set Nx SHAs
uses: nrwl/nx-set-shas@v4
Expand Down Expand Up @@ -86,11 +82,12 @@ jobs:
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v6
- name: Setup Node + Yarn
uses: ./.github/actions/setup-node-yarn
with:
node-version-file: ".nvmrc"
cache: "yarn"
# node_modules is restored from the build job's cache below.
install: "false"

- name: Restore build artifacts
uses: actions/cache/restore@v4
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@ yarn-error.log
**/.cache/

.claude

# Yarn (berry / yarn 4) — ignore caches/state, KEEP patches/releases/plugins/sdks
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.*
yarn-debug.log*
.yarn/install-state.gz
Binary file added .yarn/install-state.gz
Binary file not shown.
3 changes: 3 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
enableScripts: true

nodeLinker: node-modules
6 changes: 6 additions & 0 deletions libs/vectoriadb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"default": "./dist/src/index.js"
},
"./worker": {
"development": "./src/worker.ts",
"types": "./dist/src/worker.d.ts",
"import": "./dist/src/worker.js",
"default": "./dist/src/worker.js"
}
},
"peerDependencies": {
Expand Down
30 changes: 29 additions & 1 deletion libs/vectoriadb/src/__tests__/similarity.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { cosineSimilarity, normalizeVector, euclideanDistance, dotProduct } from '../similarity.utils';
import {
cosineSimilarity,
normalizeVector,
euclideanDistance,
dotProduct,
maxNegativeSimilarity,
} from '../similarity.utils';

describe('Similarity Utils', () => {
describe('maxNegativeSimilarity', () => {
test('returns 0 when there are no negative vectors', () => {
expect(maxNegativeSimilarity(new Float32Array([1, 0, 0]), [])).toBe(0);
});

test('returns the greatest similarity across the negatives', () => {
const doc = new Float32Array([1, 1, 0]);
const negatives = [
new Float32Array([0, 0, 1]), // orthogonal → 0
new Float32Array([1, 1, 0]), // identical → 1
new Float32Array([1, 0, 0]), // ~0.707
];
expect(maxNegativeSimilarity(doc, negatives)).toBeCloseTo(1.0, 5);
});

test('clamps to 0 (never negative) for purely opposing negatives', () => {
const doc = new Float32Array([1, 0, 0]);
const negatives = [new Float32Array([-1, 0, 0])]; // cosine -1
expect(maxNegativeSimilarity(doc, negatives)).toBe(0);
});
});

describe('cosineSimilarity', () => {
test('should return 1 for identical vectors', () => {
const a = new Float32Array([1, 2, 3, 4]);
Expand Down
67 changes: 67 additions & 0 deletions libs/vectoriadb/src/__tests__/vectoria-tfidf-negative.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TFIDFVectoria } from '../vectoria-tfidf';

/**
* Negative / anti-query ranking on the worker-safe TF-IDF path:
* `score = sim(doc, query) − negativeWeight · maxᵢ sim(doc, negativeᵢ)`.
* This is what powers "find skills for X but NOT Y" (e.g. add a policy, but not
* the enforcement ones).
*/
describe('TFIDFVectoria — negative/anti-query ranking', () => {
function db(): TFIDFVectoria {
const v = new TFIDFVectoria({ defaultSimilarityThreshold: 0 });
v.addDocument('add-policy', 'Add and create a new policy. Register policy definitions in the system.', {});
v.addDocument(
'enforce-policy',
'Enforcement policy that enforces and blocks access. Reject and deny unauthorized requests.',
{},
);
v.addDocument('user-profile', 'Manage the user profile. Update display name, avatar and email address.', {});
return v;
}

it('returns both policy skills for the positive query alone', () => {
const results = db().search('policy', { threshold: 0 });
const ids = results.map((r) => r.id);
expect(ids).toContain('add-policy');
expect(ids).toContain('enforce-policy');
});

it('demotes the enforcement skill when "enforcement" is a negative query', () => {
const v = db();
const plain = v.search('policy', { threshold: 0 });
const filtered = v.search('policy', { negativeQuery: 'enforcement enforce block access', threshold: 0 });

const enforcePlain = plain.find((r) => r.id === 'enforce-policy')!;
const enforceFiltered = filtered.find((r) => r.id === 'enforce-policy');

// The non-enforcement policy now ranks first…
expect(filtered[0].id).toBe('add-policy');
// …and the enforcement skill is penalized (lower score, or dropped entirely).
if (enforceFiltered) {
expect(enforceFiltered.score).toBeLessThan(enforcePlain.score);
expect(enforceFiltered.score).toBeLessThan(filtered[0].score);
}
});

it('drops anti-query-dominated results below a positive threshold', () => {
const v = db();
const filtered = v.search('policy', { negativeQuery: 'enforcement enforce block access', threshold: 0.05 });
// With a positive threshold, a skill more similar to the anti-query than the
// query falls out entirely.
expect(filtered.map((r) => r.id)).not.toContain('enforce-policy');
expect(filtered.map((r) => r.id)).toContain('add-policy');
});

it('accepts multiple negative queries (array)', () => {
const v = db();
const filtered = v.search('policy', { negativeQuery: ['enforcement', 'block access'], threshold: 0 });
expect(filtered[0].id).toBe('add-policy');
});

it('negativeWeight: 0 disables the penalty (equivalent to no negative)', () => {
const v = db();
const plain = v.search('policy', { threshold: 0 });
const weighted = v.search('policy', { negativeQuery: 'enforcement', negativeWeight: 0, threshold: 0 });
expect(weighted.map((r) => r.id)).toEqual(plain.map((r) => r.id));
});
});
Loading
Loading