@zenera/rag 1.1.3 → 1.1.5
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.
- package/README.md +68 -9
- package/dist/command.js +290 -32
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/schema/build.d.ts +2 -0
- package/dist/schema/build.js +62 -34
- package/dist/schema/entities.d.ts +2 -0
- package/dist/schema/entities.js +2 -1
- package/dist/schema/files.d.ts +17 -2
- package/dist/schema/files.js +36 -7
- package/dist/schema/lookup.d.ts +44 -0
- package/dist/schema/lookup.js +83 -0
- package/dist/schema/match.d.ts +36 -0
- package/dist/schema/match.js +85 -0
- package/dist/schema/progress.d.ts +26 -0
- package/dist/schema/progress.js +316 -0
- package/dist/schema/spec.d.ts +5 -0
- package/dist/schema/spec.js +34 -10
- package/dist/schema/subgraph.d.ts +6 -0
- package/dist/schema/subgraph.js +27 -0
- package/dist/schema/tools.js +140 -28
- package/package.json +3 -3
package/dist/schema/build.js
CHANGED
|
@@ -1,42 +1,67 @@
|
|
|
1
1
|
import { toEntities } from "./entities.js";
|
|
2
|
-
import { INDEX_VERSION, writeIndex, } from "./files.js";
|
|
2
|
+
import { INDEX_VERSION, SOURCES_DIR, writeIndex, } from "./files.js";
|
|
3
3
|
import { buildGraph } from "./graph.js";
|
|
4
|
+
import { beginBuild } from "./progress.js";
|
|
4
5
|
import { loadSpecs } from "./spec.js";
|
|
5
6
|
import { writeStore } from "./store.js";
|
|
6
7
|
const DEFAULT_BATCH = 96;
|
|
7
8
|
export async function buildIndex(options) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
sources: sourcesOf(corpus, entities, options.files),
|
|
13
|
-
counts: {
|
|
14
|
-
methods: entities.filter((e) => e.kind === 'method').length,
|
|
15
|
-
types: entities.filter((e) => e.kind === 'type').length,
|
|
16
|
-
properties: entities.filter((e) => e.kind === 'property').length,
|
|
17
|
-
entities: entities.length,
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
options.onRead?.(summary);
|
|
21
|
-
const vectors = await embedAll(entities, options);
|
|
22
|
-
const written = await writeStore(options.out, entities, vectors);
|
|
23
|
-
const manifest = {
|
|
24
|
-
version: INDEX_VERSION,
|
|
25
|
-
createdAt: new Date().toISOString(),
|
|
9
|
+
const journal = beginBuild({
|
|
10
|
+
dir: options.out,
|
|
11
|
+
files: options.files,
|
|
12
|
+
embedding: options.embeddingRef ?? options.embedder.id,
|
|
26
13
|
indexer: options.indexer,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
const corpus = await loadSpecs(options.files);
|
|
17
|
+
journal.phase('graph');
|
|
18
|
+
const { graph, types } = buildGraph(corpus);
|
|
19
|
+
const entities = toEntities(graph);
|
|
20
|
+
const keep = options.sources !== false;
|
|
21
|
+
const summary = {
|
|
22
|
+
sources: sourcesOf(corpus, entities, keep),
|
|
23
|
+
counts: {
|
|
24
|
+
methods: entities.filter((e) => e.kind === 'method').length,
|
|
25
|
+
types: entities.filter((e) => e.kind === 'type').length,
|
|
26
|
+
properties: entities.filter((e) => e.kind === 'property').length,
|
|
27
|
+
entities: entities.length,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
journal.read(summary.counts);
|
|
31
|
+
options.onRead?.(summary);
|
|
32
|
+
journal.phase('embedding');
|
|
33
|
+
const vectors = await embedAll(entities, options, journal);
|
|
34
|
+
journal.phase('writing');
|
|
35
|
+
const written = await writeStore(options.out, entities, vectors);
|
|
36
|
+
const manifest = {
|
|
37
|
+
version: INDEX_VERSION,
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
indexer: options.indexer,
|
|
40
|
+
embedding: {
|
|
41
|
+
ref: options.embeddingRef ?? options.embedder.id,
|
|
42
|
+
id: options.embedder.id,
|
|
43
|
+
dimensions: vectors[0]?.length ?? 0,
|
|
44
|
+
},
|
|
45
|
+
sources: summary.sources,
|
|
46
|
+
counts: summary.counts,
|
|
47
|
+
indexes: { fts: written.fts, vector: written.vector },
|
|
48
|
+
};
|
|
49
|
+
await writeIndex(options.out, {
|
|
50
|
+
manifest,
|
|
51
|
+
graph,
|
|
52
|
+
types,
|
|
53
|
+
operations: corpus.operations,
|
|
54
|
+
documents: keep ? corpus.documents : {},
|
|
55
|
+
});
|
|
56
|
+
journal.finish(manifest);
|
|
57
|
+
return { manifest, entities };
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
journal.fail(err);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
38
63
|
}
|
|
39
|
-
async function embedAll(entities, options) {
|
|
64
|
+
async function embedAll(entities, options, journal) {
|
|
40
65
|
const size = options.batch ?? DEFAULT_BATCH;
|
|
41
66
|
const out = [];
|
|
42
67
|
for (let at = 0; at < entities.length; at += size) {
|
|
@@ -50,6 +75,7 @@ async function embedAll(entities, options) {
|
|
|
50
75
|
throw new Error(`${options.embedder.id} answered ${response.vectors.length} vectors for ${slice.length} texts`);
|
|
51
76
|
}
|
|
52
77
|
out.push(...response.vectors.map((v) => Float32Array.from(v)));
|
|
78
|
+
journal.progress(out.length, entities.length);
|
|
53
79
|
options.onProgress?.(out.length, entities.length);
|
|
54
80
|
}
|
|
55
81
|
return out;
|
|
@@ -59,12 +85,14 @@ async function embedAll(entities, options) {
|
|
|
59
85
|
* never named — an inline request body, say — is credited to the file it came
|
|
60
86
|
* from instead of going missing.
|
|
61
87
|
*/
|
|
62
|
-
function sourcesOf(corpus, entities,
|
|
63
|
-
return corpus.docs.map((doc
|
|
88
|
+
function sourcesOf(corpus, entities, keep) {
|
|
89
|
+
return corpus.docs.map((doc) => {
|
|
64
90
|
const mine = entities.filter((e) => e.source === doc.source);
|
|
65
91
|
const operations = corpus.operations.filter((op) => op.source === doc.source);
|
|
66
92
|
return {
|
|
67
|
-
|
|
93
|
+
name: doc.source,
|
|
94
|
+
file: doc.file,
|
|
95
|
+
path: keep ? `${SOURCES_DIR}/${doc.source}.json` : undefined,
|
|
68
96
|
sha256: doc.sha256,
|
|
69
97
|
dialect: doc.dialect,
|
|
70
98
|
title: doc.title,
|
|
@@ -15,4 +15,6 @@ export interface EntityRecord {
|
|
|
15
15
|
text: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function toEntities(graph: ApiGraph): EntityRecord[];
|
|
18
|
+
/** Exported so a literal search reads the same string the index was built from. */
|
|
19
|
+
export declare function textOf(graph: ApiGraph, id: string): string;
|
|
18
20
|
//# sourceMappingURL=entities.d.ts.map
|
package/dist/schema/entities.js
CHANGED
|
@@ -22,7 +22,8 @@ export function toEntities(graph) {
|
|
|
22
22
|
}
|
|
23
23
|
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
/** Exported so a literal search reads the same string the index was built from. */
|
|
26
|
+
export function textOf(graph, id) {
|
|
26
27
|
const a = graph.getNodeAttributes(id);
|
|
27
28
|
const parts = [];
|
|
28
29
|
if (a.kind === 'method') {
|
package/dist/schema/files.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { ApiGraph } from './graph.ts';
|
|
2
2
|
import type { Schema } from './schema.ts';
|
|
3
3
|
import type { Operation } from './spec.ts';
|
|
4
|
-
export declare const INDEX_VERSION =
|
|
4
|
+
export declare const INDEX_VERSION = 3;
|
|
5
5
|
export declare const MANIFEST_FILE = "manifest.json";
|
|
6
6
|
export declare const GRAPH_FILE = "graph.json";
|
|
7
7
|
export declare const SCHEMAS_FILE = "schemas.json";
|
|
8
8
|
export declare const OPERATIONS_FILE = "operations.json";
|
|
9
9
|
export declare const LANCE_DIR = "lance";
|
|
10
|
+
export declare const SOURCES_DIR = "sources";
|
|
10
11
|
export interface SourceRecord {
|
|
11
|
-
|
|
12
|
+
/** the document's name within the index: what every entity's `source` says */
|
|
13
|
+
name: string;
|
|
14
|
+
/** what the file was called on the machine that built this */
|
|
15
|
+
file: string;
|
|
16
|
+
/** the bundled copy, relative to the index; absent when none was kept */
|
|
17
|
+
path?: string;
|
|
12
18
|
sha256: string;
|
|
13
19
|
dialect: string;
|
|
14
20
|
title: string;
|
|
@@ -47,6 +53,8 @@ export interface WrittenIndex {
|
|
|
47
53
|
graph: ApiGraph;
|
|
48
54
|
types: Readonly<Record<string, Schema>>;
|
|
49
55
|
operations: readonly Operation[];
|
|
56
|
+
/** the bundled documents to keep beside the index, by name */
|
|
57
|
+
documents: Readonly<Record<string, string>>;
|
|
50
58
|
}
|
|
51
59
|
export interface OpenIndex {
|
|
52
60
|
dir: string;
|
|
@@ -60,6 +68,13 @@ export interface OpenIndex {
|
|
|
60
68
|
export declare const lancePath: (dir: string) => string;
|
|
61
69
|
export declare function writeIndex(dir: string, index: WrittenIndex): Promise<void>;
|
|
62
70
|
export declare function openIndex(dir: string): Promise<OpenIndex>;
|
|
71
|
+
/**
|
|
72
|
+
* The bundled document as it was indexed. Kept only when the index was built
|
|
73
|
+
* with sources, which is why the manifest is asked first: the difference
|
|
74
|
+
* between "no such document" and "this index did not keep them" is the whole
|
|
75
|
+
* of what the caller can do next.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readSource(dir: string, name: string): Promise<string | undefined>;
|
|
63
78
|
export declare function readManifest(dir: string): Promise<Manifest>;
|
|
64
79
|
/**
|
|
65
80
|
* A store answers with the neighbours of a vector, and a vector means nothing
|
package/dist/schema/files.js
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
|
+
import { CliError, EXIT } from '@zenera/cli/lib';
|
|
1
2
|
import { MultiDirectedGraph } from 'graphology';
|
|
2
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
|
-
import { CliError, EXIT } from '@zenera/cli/lib';
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// What an index is, on disk
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// operations.
|
|
8
|
+
// A directory, and the order it is written in is the whole crash story: the
|
|
9
|
+
// manifest goes last, so a half-built index has no manifest and reads as "not
|
|
10
|
+
// indexed" rather than as a store that quietly lost half its operations.
|
|
12
11
|
//
|
|
13
12
|
// `graph.json` is deliberately thin — names, directions, edges — because it is
|
|
14
13
|
// parsed whole on every search. The schemas are the bulk of the bytes and are
|
|
15
14
|
// wanted only when something is being printed, so they live apart and are read
|
|
16
15
|
// on first use.
|
|
16
|
+
//
|
|
17
|
+
// `sources/` holds the documents themselves, bundled, so the index is one
|
|
18
|
+
// portable thing: nothing in it names a path outside itself, it can be moved or
|
|
19
|
+
// shipped whole, and the graph can be rebuilt — or re-embedded with another
|
|
20
|
+
// model — without going looking for the files it was made from.
|
|
17
21
|
// ---------------------------------------------------------------------------
|
|
18
|
-
export const INDEX_VERSION =
|
|
22
|
+
export const INDEX_VERSION = 3;
|
|
19
23
|
export const MANIFEST_FILE = 'manifest.json';
|
|
20
24
|
export const GRAPH_FILE = 'graph.json';
|
|
21
25
|
export const SCHEMAS_FILE = 'schemas.json';
|
|
22
26
|
export const OPERATIONS_FILE = 'operations.json';
|
|
23
27
|
export const LANCE_DIR = 'lance';
|
|
28
|
+
export const SOURCES_DIR = 'sources';
|
|
24
29
|
export const lancePath = (dir) => join(dir, LANCE_DIR);
|
|
25
30
|
export async function writeIndex(dir, index) {
|
|
26
31
|
await mkdir(dir, { recursive: true });
|
|
27
32
|
await writeFile(join(dir, GRAPH_FILE), JSON.stringify(index.graph.export()));
|
|
28
33
|
await writeFile(join(dir, SCHEMAS_FILE), JSON.stringify(index.types));
|
|
29
34
|
await writeFile(join(dir, OPERATIONS_FILE), JSON.stringify(index.operations));
|
|
35
|
+
const documents = Object.entries(index.documents);
|
|
36
|
+
if (documents.length > 0) {
|
|
37
|
+
await mkdir(join(dir, SOURCES_DIR), { recursive: true });
|
|
38
|
+
for (const [name, text] of documents) {
|
|
39
|
+
await writeFile(join(dir, SOURCES_DIR, `${name}.json`), text);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(index.manifest, null, 4)}\n`);
|
|
31
43
|
}
|
|
32
44
|
export async function openIndex(dir) {
|
|
@@ -48,6 +60,23 @@ function once(load) {
|
|
|
48
60
|
let pending;
|
|
49
61
|
return () => (pending ??= load());
|
|
50
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The bundled document as it was indexed. Kept only when the index was built
|
|
65
|
+
* with sources, which is why the manifest is asked first: the difference
|
|
66
|
+
* between "no such document" and "this index did not keep them" is the whole
|
|
67
|
+
* of what the caller can do next.
|
|
68
|
+
*/
|
|
69
|
+
export async function readSource(dir, name) {
|
|
70
|
+
const manifest = await readManifest(dir);
|
|
71
|
+
const record = manifest.sources.find((s) => s.name === name);
|
|
72
|
+
if (!record) {
|
|
73
|
+
throw new CliError(`${dir} holds no document called ${name}`, EXIT.failed, `it has: ${manifest.sources.map((s) => s.name).join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
if (!record.path) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return await readFile(join(dir, record.path), 'utf8');
|
|
79
|
+
}
|
|
51
80
|
export async function readManifest(dir) {
|
|
52
81
|
let text;
|
|
53
82
|
try {
|
|
@@ -58,7 +87,7 @@ export async function readManifest(dir) {
|
|
|
58
87
|
}
|
|
59
88
|
const manifest = JSON.parse(text);
|
|
60
89
|
if (manifest.version !== INDEX_VERSION) {
|
|
61
|
-
throw new CliError(`${dir}
|
|
90
|
+
throw new CliError(`${dir} is a version ${manifest.version} index, and this indexer reads version ${INDEX_VERSION}`, EXIT.invalid, 'rebuild it with `zen rag schema index`');
|
|
62
91
|
}
|
|
63
92
|
return manifest;
|
|
64
93
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ApiGraph, NodeAttrs, NodeKind } from './graph.ts';
|
|
2
|
+
import { type Matcher } from './match.ts';
|
|
3
|
+
export interface Row extends NodeAttrs {
|
|
4
|
+
id: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ListFilter {
|
|
7
|
+
kind: NodeKind;
|
|
8
|
+
/** any one matching is enough; none means every name passes */
|
|
9
|
+
name?: readonly Matcher[];
|
|
10
|
+
path?: readonly Matcher[];
|
|
11
|
+
source?: string;
|
|
12
|
+
methodType?: string;
|
|
13
|
+
direction?: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface Listing {
|
|
17
|
+
/** how many matched, whatever was kept */
|
|
18
|
+
found: number;
|
|
19
|
+
rows: Row[];
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface Match {
|
|
23
|
+
id: string;
|
|
24
|
+
attributes: NodeAttrs;
|
|
25
|
+
/** the indexed text this matched against */
|
|
26
|
+
text: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GrepFilter {
|
|
29
|
+
kinds?: readonly string[];
|
|
30
|
+
source?: string;
|
|
31
|
+
limit?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface Grep {
|
|
34
|
+
found: number;
|
|
35
|
+
matches: Match[];
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function listNodes(graph: ApiGraph, filter: ListFilter): Listing;
|
|
39
|
+
export declare function grepNodes(graph: ApiGraph, match: Matcher, filter?: GrepFilter): Grep;
|
|
40
|
+
/** How many fields a type carries, which is most of what a listing wants to say. */
|
|
41
|
+
export declare function propertyCount(graph: ApiGraph, id: string): number;
|
|
42
|
+
/** That count, said properly, in the one phrasing the CLI and the tools share. */
|
|
43
|
+
export declare const fields: (n: number) => string;
|
|
44
|
+
//# sourceMappingURL=lookup.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { textOf } from "./entities.js";
|
|
2
|
+
import { PatternError } from "./match.js";
|
|
3
|
+
/** A scan is bounded, because a pattern may have come from a model. */
|
|
4
|
+
const DEADLINE_MS = 2000;
|
|
5
|
+
const DEADLINE_EVERY = 500;
|
|
6
|
+
export function listNodes(graph, filter) {
|
|
7
|
+
const rows = [];
|
|
8
|
+
graph.forEachNode((id, a) => {
|
|
9
|
+
if (a.kind !== filter.kind || !passes(a, filter)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (filter.name && !filter.name.some((match) => match(a.name))) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (filter.path && !filter.path.some((match) => match(a.path))) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
rows.push({ ...a, id });
|
|
19
|
+
});
|
|
20
|
+
rows.sort(order(filter.kind));
|
|
21
|
+
return cut(rows, filter.limit);
|
|
22
|
+
}
|
|
23
|
+
export function grepNodes(graph, match, filter = {}) {
|
|
24
|
+
const kinds = filter.kinds?.length ? new Set(filter.kinds) : undefined;
|
|
25
|
+
const matches = [];
|
|
26
|
+
const until = Date.now() + DEADLINE_MS;
|
|
27
|
+
let seen = 0;
|
|
28
|
+
for (const id of graph.nodes()) {
|
|
29
|
+
if (++seen % DEADLINE_EVERY === 0 && Date.now() > until) {
|
|
30
|
+
throw new PatternError(`the pattern is still running after ${DEADLINE_MS / 1000}s — it is too expensive to be useful`);
|
|
31
|
+
}
|
|
32
|
+
const a = graph.getNodeAttributes(id);
|
|
33
|
+
if (kinds && !kinds.has(a.kind)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (filter.source && a.source !== filter.source) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const text = textOf(graph, id);
|
|
40
|
+
if (match(text)) {
|
|
41
|
+
matches.push({ id, attributes: a, text });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
matches.sort((a, b) => a.id.localeCompare(b.id));
|
|
45
|
+
const kept = cut(matches, filter.limit);
|
|
46
|
+
return { found: kept.found, matches: kept.rows, truncated: kept.truncated };
|
|
47
|
+
}
|
|
48
|
+
/** How many fields a type carries, which is most of what a listing wants to say. */
|
|
49
|
+
export function propertyCount(graph, id) {
|
|
50
|
+
return graph
|
|
51
|
+
.outEdges(id)
|
|
52
|
+
.filter((e) => graph.getEdgeAttribute(e, 'relation') === 'HAS_PROPERTY').length;
|
|
53
|
+
}
|
|
54
|
+
/** That count, said properly, in the one phrasing the CLI and the tools share. */
|
|
55
|
+
export const fields = (n) => `${n} ${n === 1 ? 'field' : 'fields'}`;
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
function passes(a, filter) {
|
|
58
|
+
if (filter.source && a.source !== filter.source) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (filter.methodType && a.methodType !== filter.methodType) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (filter.direction && a.direction !== filter.direction && a.direction !== 'both') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
/** Operations read as a table of routes; everything else reads as a list of names. */
|
|
70
|
+
function order(kind) {
|
|
71
|
+
if (kind !== 'method') {
|
|
72
|
+
return (a, b) => a.id.localeCompare(b.id);
|
|
73
|
+
}
|
|
74
|
+
return (a, b) => a.path.localeCompare(b.path) || a.httpMethod.localeCompare(b.httpMethod);
|
|
75
|
+
}
|
|
76
|
+
function cut(rows, limit) {
|
|
77
|
+
const found = rows.length;
|
|
78
|
+
if (!limit || limit >= found) {
|
|
79
|
+
return { found, rows, truncated: false };
|
|
80
|
+
}
|
|
81
|
+
return { found, rows: rows.slice(0, limit), truncated: true };
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=lookup.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Long enough for any honest pattern, short enough to bound a bad one. */
|
|
2
|
+
export declare const MAX_PATTERN = 200;
|
|
3
|
+
export declare class PatternError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export interface MatchOptions {
|
|
6
|
+
/** read the pattern as a regular expression rather than as a literal */
|
|
7
|
+
regex?: boolean;
|
|
8
|
+
caseSensitive?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type Matcher = (text: string) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* A predicate over a string. Literal by default: someone typing `user.id` means
|
|
13
|
+
* those seven characters, and a dot that quietly matched anything would be a
|
|
14
|
+
* worse answer than no answer.
|
|
15
|
+
*/
|
|
16
|
+
export declare function matcher(pattern: string, options?: MatchOptions): Matcher;
|
|
17
|
+
/**
|
|
18
|
+
* A glob, matched against the whole string. Globs rather than regexes because
|
|
19
|
+
* these are for naming things — a path, a schema — and a star is what everyone
|
|
20
|
+
* reaches for first.
|
|
21
|
+
*/
|
|
22
|
+
export declare function wildcard(pattern: string, options?: {
|
|
23
|
+
caseSensitive?: boolean;
|
|
24
|
+
}): Matcher;
|
|
25
|
+
/** Whether a pattern is asking to be read as a glob at all. */
|
|
26
|
+
export declare const isGlob: (pattern: string) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* What someone means when they type a name into a filter. With a star in it,
|
|
29
|
+
* a glob; without one, a substring — because `password` typed into `--name` is
|
|
30
|
+
* a search for the word, and a whole-string match would answer nothing and
|
|
31
|
+
* look like the field does not exist.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loose(pattern: string, options?: MatchOptions): Matcher;
|
|
34
|
+
/** True when any of the patterns matches; no patterns means no opinion. */
|
|
35
|
+
export declare function anyOf(matchers: readonly Matcher[]): Matcher | undefined;
|
|
36
|
+
//# sourceMappingURL=match.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Matching, with nothing learned in between
|
|
3
|
+
//
|
|
4
|
+
// Everything here is exact. A glob matches the characters it names and a
|
|
5
|
+
// substring is a substring, which is the whole point of the surfaces built on
|
|
6
|
+
// it: a vector index answers "what is near this", and near is a ranking, so it
|
|
7
|
+
// can only ever return the top of a list. When the question is "does the word
|
|
8
|
+
// `password` appear anywhere at all", a ranking is the wrong instrument and no
|
|
9
|
+
// amount of tuning makes it the right one.
|
|
10
|
+
//
|
|
11
|
+
// A pattern may arrive from a model, so a regex is a bounded promise: the
|
|
12
|
+
// length is capped here and the scan that uses it keeps a deadline.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** Long enough for any honest pattern, short enough to bound a bad one. */
|
|
15
|
+
export const MAX_PATTERN = 200;
|
|
16
|
+
export class PatternError extends Error {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A predicate over a string. Literal by default: someone typing `user.id` means
|
|
20
|
+
* those seven characters, and a dot that quietly matched anything would be a
|
|
21
|
+
* worse answer than no answer.
|
|
22
|
+
*/
|
|
23
|
+
export function matcher(pattern, options = {}) {
|
|
24
|
+
guard(pattern);
|
|
25
|
+
if (!options.regex) {
|
|
26
|
+
if (options.caseSensitive) {
|
|
27
|
+
return (text) => text.includes(pattern);
|
|
28
|
+
}
|
|
29
|
+
const needle = pattern.toLowerCase();
|
|
30
|
+
return (text) => text.toLowerCase().includes(needle);
|
|
31
|
+
}
|
|
32
|
+
const expression = compile(pattern, options.caseSensitive ? '' : 'i');
|
|
33
|
+
// `lastIndex` is not carried between calls: the flags never include `g`.
|
|
34
|
+
return (text) => expression.test(text);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A glob, matched against the whole string. Globs rather than regexes because
|
|
38
|
+
* these are for naming things — a path, a schema — and a star is what everyone
|
|
39
|
+
* reaches for first.
|
|
40
|
+
*/
|
|
41
|
+
export function wildcard(pattern, options = {}) {
|
|
42
|
+
guard(pattern);
|
|
43
|
+
const source = [...pattern]
|
|
44
|
+
.map((char) => (char === '*' ? '.*' : char === '?' ? '.' : escape(char)))
|
|
45
|
+
.join('');
|
|
46
|
+
const expression = compile(`^${source}$`, options.caseSensitive ? '' : 'i');
|
|
47
|
+
return (text) => expression.test(text);
|
|
48
|
+
}
|
|
49
|
+
/** Whether a pattern is asking to be read as a glob at all. */
|
|
50
|
+
export const isGlob = (pattern) => /[*?]/.test(pattern);
|
|
51
|
+
/**
|
|
52
|
+
* What someone means when they type a name into a filter. With a star in it,
|
|
53
|
+
* a glob; without one, a substring — because `password` typed into `--name` is
|
|
54
|
+
* a search for the word, and a whole-string match would answer nothing and
|
|
55
|
+
* look like the field does not exist.
|
|
56
|
+
*/
|
|
57
|
+
export function loose(pattern, options = {}) {
|
|
58
|
+
return isGlob(pattern) ? wildcard(pattern, options) : matcher(pattern, options);
|
|
59
|
+
}
|
|
60
|
+
/** True when any of the patterns matches; no patterns means no opinion. */
|
|
61
|
+
export function anyOf(matchers) {
|
|
62
|
+
if (matchers.length === 0) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return (text) => matchers.some((match) => match(text));
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function guard(pattern) {
|
|
69
|
+
if (pattern.length === 0) {
|
|
70
|
+
throw new PatternError('the pattern is empty');
|
|
71
|
+
}
|
|
72
|
+
if (pattern.length > MAX_PATTERN) {
|
|
73
|
+
throw new PatternError(`the pattern is longer than ${MAX_PATTERN} characters`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function compile(source, flags) {
|
|
77
|
+
try {
|
|
78
|
+
return new RegExp(source, flags);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
throw new PatternError(`invalid pattern: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const escape = (char) => char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
|
+
//# sourceMappingURL=match.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Counts, Manifest } from './files.ts';
|
|
2
|
+
export declare const LOCK_FILE = ".lock";
|
|
3
|
+
export declare const README_FILE = "README.md";
|
|
4
|
+
export type Phase = 'reading' | 'graph' | 'embedding' | 'writing';
|
|
5
|
+
export interface BuildPlan {
|
|
6
|
+
/** where the index goes */
|
|
7
|
+
dir: string;
|
|
8
|
+
files: readonly string[];
|
|
9
|
+
/** the embedding reference as it was typed */
|
|
10
|
+
embedding: string;
|
|
11
|
+
indexer: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Journal {
|
|
14
|
+
phase(name: Phase): void;
|
|
15
|
+
/** what the documents turned out to hold, once they have been read */
|
|
16
|
+
read(counts: Counts): void;
|
|
17
|
+
progress(done: number, total: number): void;
|
|
18
|
+
finish(manifest: Manifest): void;
|
|
19
|
+
fail(reason: unknown): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Takes the directory, or refuses it. Two builds writing one index would
|
|
23
|
+
* interleave their LanceDB writes and leave a store neither of them describes.
|
|
24
|
+
*/
|
|
25
|
+
export declare function beginBuild(plan: BuildPlan): Journal;
|
|
26
|
+
//# sourceMappingURL=progress.d.ts.map
|