feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,54 @@
import TestEmbeddingProvider from './TestEmbeddingProvider';
// Sanity tests for the test stub. Not exhaustive — the goal is just to
// confirm the properties the indexer/search integration tests depend on:
// determinism, similarity for related text, distance for unrelated text.
const dot = (a: number[], b: number[]): number => {
let sum = 0;
for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
return sum;
};
describe('TestEmbeddingProvider', () => {
it('produces vectors of the configured dimension', async () => {
const provider = new TestEmbeddingProvider({ dimension: 16 });
const [v] = await provider.embed(['hello world']);
expect(v.length).toBe(16);
});
it('is deterministic: same input always produces the same vector', async () => {
const provider = new TestEmbeddingProvider();
const [v1] = await provider.embed(['the quick brown fox']);
const [v2] = await provider.embed(['the quick brown fox']);
expect(v1).toEqual(v2);
});
it('returns unit-length vectors (cosine similarity == dot product)', async () => {
const provider = new TestEmbeddingProvider();
const [v] = await provider.embed(['anything reasonable']);
const norm = Math.sqrt(dot(v, v));
expect(norm).toBeCloseTo(1, 5);
});
it('scores related text more similarly than unrelated text', async () => {
const provider = new TestEmbeddingProvider();
const [vQuery, vRelated, vUnrelated] = await provider.embed([
'cats and kittens',
'kittens like cats',
'engine repair manual',
]);
const relatedSim = dot(vQuery, vRelated);
const unrelatedSim = dot(vQuery, vUnrelated);
expect(relatedSim).toBeGreaterThan(unrelatedSim);
});
it('handles empty and single-character inputs without producing zero vectors', async () => {
const provider = new TestEmbeddingProvider();
const [vEmpty, vSingle] = await provider.embed(['', 'a']);
expect(Math.sqrt(dot(vEmpty, vEmpty))).toBeCloseTo(1, 5);
expect(Math.sqrt(dot(vSingle, vSingle))).toBeCloseTo(1, 5);
});
});

查看文件

@@ -0,0 +1,86 @@
import { EmbeddingProvider, ProviderClassification } from '../types';
// Deterministic embedding provider used in CI tests for the indexer and the
// search layer. Same text always produces the same vector, and texts that share
// characters produce more-similar vectors than texts that don't — so
// similaritySearch() returns sensible orderings.
//
// This is NOT a real embedding model. It can't capture semantics that span
// multiple words or handle synonyms. It exists purely so we can exercise the
// indexing + search pipeline end-to-end in CI without bundling onnxruntime-node
// or a 100MB model file.
const DEFAULT_DIMENSION = 32;
const hashString = (s: string): number => {
// Simple FNV-1a 32-bit hash, used to map each character bigram to a vector
// index. Deterministic and fast.
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
};
const l2Norm = (v: number[]): number => {
let sum = 0;
for (const x of v) sum += x * x;
return Math.sqrt(sum);
};
const normalise = (v: number[]): number[] => {
const norm = l2Norm(v);
if (norm === 0) return v;
return v.map(x => x / norm);
};
const addUnitX = (v: number[]) => {
return v.map((x, i) => (i === 0 ? x + 1 : x));
};
const embedOne = (text: string, dimension: number): number[] => {
const vec = new Array<number>(dimension).fill(0);
const lower = text.toLowerCase();
// Character bigrams give a richer signal than unigrams (which would
// collapse "cat" and "act" together) without exploding the keyspace.
for (let i = 0; i < lower.length - 1; i++) {
const bigram = lower.slice(i, i + 2);
const bucket = hashString(bigram) % dimension;
vec[bucket] += 1;
}
// Always include a constant signal so empty or one-character strings
// still produce a non-zero vector (otherwise normalise() returns zeros
// and similarity is undefined).
vec[0] += 1;
// Add a constant post-normalization to increase the similarity scores to more
// closely match the production model (which distributes scores roughly sin [0.7, 1])
return normalise(
addUnitX(normalise(vec)),
);
};
export interface TestEmbeddingProviderOptions {
id?: string;
modelId?: string;
dimension?: number;
}
export default class TestEmbeddingProvider implements EmbeddingProvider {
public id: string;
public modelId: string;
public dimension: number;
public classification: ProviderClassification = 'local';
public constructor(options: TestEmbeddingProviderOptions = {}) {
this.id = options.id ?? 'test';
this.modelId = options.modelId ?? 'test-model-v1';
this.dimension = options.dimension ?? DEFAULT_DIMENSION;
}
public async embed(texts: string[]): Promise<number[][]> {
return texts.map(t => embedOne(t, this.dimension));
}
}