@indigoai-us/cq 0.0.1
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/dist/__tests__/integration.test.d.ts +8 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +233 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/budget/__tests__/budget.test.d.ts +2 -0
- package/dist/budget/__tests__/budget.test.d.ts.map +1 -0
- package/dist/budget/__tests__/budget.test.js +238 -0
- package/dist/budget/__tests__/budget.test.js.map +1 -0
- package/dist/budget/__tests__/token-tracker.test.d.ts +2 -0
- package/dist/budget/__tests__/token-tracker.test.d.ts.map +1 -0
- package/dist/budget/__tests__/token-tracker.test.js +292 -0
- package/dist/budget/__tests__/token-tracker.test.js.map +1 -0
- package/dist/budget/index.d.ts +38 -0
- package/dist/budget/index.d.ts.map +1 -0
- package/dist/budget/index.js +192 -0
- package/dist/budget/index.js.map +1 -0
- package/dist/budget/token-tracker.d.ts +62 -0
- package/dist/budget/token-tracker.d.ts.map +1 -0
- package/dist/budget/token-tracker.js +152 -0
- package/dist/budget/token-tracker.js.map +1 -0
- package/dist/budget/types.d.ts +41 -0
- package/dist/budget/types.d.ts.map +1 -0
- package/dist/budget/types.js +5 -0
- package/dist/budget/types.js.map +1 -0
- package/dist/confidence/__tests__/confidence.test.d.ts +2 -0
- package/dist/confidence/__tests__/confidence.test.d.ts.map +1 -0
- package/dist/confidence/__tests__/confidence.test.js +336 -0
- package/dist/confidence/__tests__/confidence.test.js.map +1 -0
- package/dist/confidence/erosion.d.ts +33 -0
- package/dist/confidence/erosion.d.ts.map +1 -0
- package/dist/confidence/erosion.js +42 -0
- package/dist/confidence/erosion.js.map +1 -0
- package/dist/confidence/index.d.ts +42 -0
- package/dist/confidence/index.d.ts.map +1 -0
- package/dist/confidence/index.js +168 -0
- package/dist/confidence/index.js.map +1 -0
- package/dist/confidence/types.d.ts +29 -0
- package/dist/confidence/types.d.ts.map +1 -0
- package/dist/confidence/types.js +5 -0
- package/dist/confidence/types.js.map +1 -0
- package/dist/curiosity/__tests__/curiosity.test.d.ts +2 -0
- package/dist/curiosity/__tests__/curiosity.test.d.ts.map +1 -0
- package/dist/curiosity/__tests__/curiosity.test.js +280 -0
- package/dist/curiosity/__tests__/curiosity.test.js.map +1 -0
- package/dist/curiosity/dedup.d.ts +29 -0
- package/dist/curiosity/dedup.d.ts.map +1 -0
- package/dist/curiosity/dedup.js +64 -0
- package/dist/curiosity/dedup.js.map +1 -0
- package/dist/curiosity/index.d.ts +56 -0
- package/dist/curiosity/index.d.ts.map +1 -0
- package/dist/curiosity/index.js +163 -0
- package/dist/curiosity/index.js.map +1 -0
- package/dist/curiosity/types.d.ts +29 -0
- package/dist/curiosity/types.d.ts.map +1 -0
- package/dist/curiosity/types.js +5 -0
- package/dist/curiosity/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +7 -0
- package/dist/index.test.js.map +1 -0
- package/dist/init.d.ts +17 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +153 -0
- package/dist/init.js.map +1 -0
- package/dist/knowledge-tree/__tests__/knowledge-tree.test.d.ts +2 -0
- package/dist/knowledge-tree/__tests__/knowledge-tree.test.d.ts.map +1 -0
- package/dist/knowledge-tree/__tests__/knowledge-tree.test.js +317 -0
- package/dist/knowledge-tree/__tests__/knowledge-tree.test.js.map +1 -0
- package/dist/knowledge-tree/format.d.ts +17 -0
- package/dist/knowledge-tree/format.d.ts.map +1 -0
- package/dist/knowledge-tree/format.js +48 -0
- package/dist/knowledge-tree/format.js.map +1 -0
- package/dist/knowledge-tree/index.d.ts +33 -0
- package/dist/knowledge-tree/index.d.ts.map +1 -0
- package/dist/knowledge-tree/index.js +182 -0
- package/dist/knowledge-tree/index.js.map +1 -0
- package/dist/knowledge-tree/types.d.ts +37 -0
- package/dist/knowledge-tree/types.d.ts.map +1 -0
- package/dist/knowledge-tree/types.js +5 -0
- package/dist/knowledge-tree/types.js.map +1 -0
- package/dist/metadata/__tests__/metadata.test.d.ts +2 -0
- package/dist/metadata/__tests__/metadata.test.d.ts.map +1 -0
- package/dist/metadata/__tests__/metadata.test.js +196 -0
- package/dist/metadata/__tests__/metadata.test.js.map +1 -0
- package/dist/metadata/__tests__/search.test.d.ts +2 -0
- package/dist/metadata/__tests__/search.test.d.ts.map +1 -0
- package/dist/metadata/__tests__/search.test.js +227 -0
- package/dist/metadata/__tests__/search.test.js.map +1 -0
- package/dist/metadata/index.d.ts +29 -0
- package/dist/metadata/index.d.ts.map +1 -0
- package/dist/metadata/index.js +108 -0
- package/dist/metadata/index.js.map +1 -0
- package/dist/metadata/schema.d.ts +8 -0
- package/dist/metadata/schema.d.ts.map +1 -0
- package/dist/metadata/schema.js +31 -0
- package/dist/metadata/schema.js.map +1 -0
- package/dist/metadata/search.d.ts +27 -0
- package/dist/metadata/search.d.ts.map +1 -0
- package/dist/metadata/search.js +57 -0
- package/dist/metadata/search.js.map +1 -0
- package/dist/metadata/types.d.ts +17 -0
- package/dist/metadata/types.d.ts.map +1 -0
- package/dist/metadata/types.js +5 -0
- package/dist/metadata/types.js.map +1 -0
- package/dist/orchestrator/__tests__/orchestrator.test.d.ts +2 -0
- package/dist/orchestrator/__tests__/orchestrator.test.d.ts.map +1 -0
- package/dist/orchestrator/__tests__/orchestrator.test.js +354 -0
- package/dist/orchestrator/__tests__/orchestrator.test.js.map +1 -0
- package/dist/orchestrator/index.d.ts +23 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/index.js +302 -0
- package/dist/orchestrator/index.js.map +1 -0
- package/dist/orchestrator/scheduler.d.ts +32 -0
- package/dist/orchestrator/scheduler.d.ts.map +1 -0
- package/dist/orchestrator/scheduler.js +102 -0
- package/dist/orchestrator/scheduler.js.map +1 -0
- package/dist/orchestrator/types.d.ts +90 -0
- package/dist/orchestrator/types.d.ts.map +1 -0
- package/dist/orchestrator/types.js +5 -0
- package/dist/orchestrator/types.js.map +1 -0
- package/dist/pipeline/__tests__/pipeline.test.d.ts +2 -0
- package/dist/pipeline/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline/__tests__/pipeline.test.js +320 -0
- package/dist/pipeline/__tests__/pipeline.test.js.map +1 -0
- package/dist/pipeline/index.d.ts +23 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +212 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/types.d.ts +41 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +5 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/providers/__tests__/providers.test.d.ts +2 -0
- package/dist/providers/__tests__/providers.test.d.ts.map +1 -0
- package/dist/providers/__tests__/providers.test.js +195 -0
- package/dist/providers/__tests__/providers.test.js.map +1 -0
- package/dist/providers/claude-model.d.ts +17 -0
- package/dist/providers/claude-model.d.ts.map +1 -0
- package/dist/providers/claude-model.js +119 -0
- package/dist/providers/claude-model.js.map +1 -0
- package/dist/providers/index.d.ts +22 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +38 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai-search.d.ts +14 -0
- package/dist/providers/openai-search.d.ts.map +1 -0
- package/dist/providers/openai-search.js +97 -0
- package/dist/providers/openai-search.js.map +1 -0
- package/dist/providers/types.d.ts +42 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +5 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/vetter/__tests__/vetter.test.d.ts +2 -0
- package/dist/vetter/__tests__/vetter.test.d.ts.map +1 -0
- package/dist/vetter/__tests__/vetter.test.js +224 -0
- package/dist/vetter/__tests__/vetter.test.js.map +1 -0
- package/dist/vetter/index.d.ts +23 -0
- package/dist/vetter/index.d.ts.map +1 -0
- package/dist/vetter/index.js +177 -0
- package/dist/vetter/index.js.map +1 -0
- package/dist/vetter/resource-tracker.d.ts +49 -0
- package/dist/vetter/resource-tracker.d.ts.map +1 -0
- package/dist/vetter/resource-tracker.js +183 -0
- package/dist/vetter/resource-tracker.js.map +1 -0
- package/dist/vetter/types.d.ts +44 -0
- package/dist/vetter/types.d.ts.map +1 -0
- package/dist/vetter/types.js +5 -0
- package/dist/vetter/types.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata Store — SQLite-backed metadata tracking for knowledge entries.
|
|
3
|
+
*
|
|
4
|
+
* Tracks confidence scores, token counts, erosion rates, and timestamps
|
|
5
|
+
* alongside the knowledge tree.
|
|
6
|
+
*/
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import { CREATE_KNOWLEDGE_META, CREATE_KNOWLEDGE_FTS } from "./schema.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Database lifecycle
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
let db = null;
|
|
13
|
+
/** Initialise (or open) the metadata SQLite database with WAL journaling. */
|
|
14
|
+
export function initMetadataDb(dbPath) {
|
|
15
|
+
db = new Database(dbPath);
|
|
16
|
+
db.pragma("journal_mode = WAL");
|
|
17
|
+
db.exec(CREATE_KNOWLEDGE_META);
|
|
18
|
+
db.exec(CREATE_KNOWLEDGE_FTS);
|
|
19
|
+
return db;
|
|
20
|
+
}
|
|
21
|
+
/** Close the database connection. */
|
|
22
|
+
export function closeDb() {
|
|
23
|
+
if (db) {
|
|
24
|
+
db.close();
|
|
25
|
+
db = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Return the current database instance (mainly for testing). */
|
|
29
|
+
export function getDb() {
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/** Estimate token count from content (~4 chars per token). */
|
|
36
|
+
function estimateTokens(content) {
|
|
37
|
+
return Math.ceil(content.length / 4);
|
|
38
|
+
}
|
|
39
|
+
/** Count sources — 1 if entry has a source URL, 0 otherwise. */
|
|
40
|
+
function countSources(entry) {
|
|
41
|
+
return entry.source ? 1 : 0;
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// CRUD operations
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Index (upsert) a KnowledgeEntry into the metadata store.
|
|
48
|
+
*
|
|
49
|
+
* Sets confidence to 1.0 and resets last_verified_at on every write.
|
|
50
|
+
*/
|
|
51
|
+
export function indexEntry(entry) {
|
|
52
|
+
if (!db)
|
|
53
|
+
throw new Error("Metadata DB not initialised — call initMetadataDb first");
|
|
54
|
+
const now = new Date().toISOString();
|
|
55
|
+
const stmt = db.prepare(`
|
|
56
|
+
INSERT INTO knowledge_meta (slug, category, title, confidence, created_at, updated_at, last_verified_at, erosion_rate, token_count, source_count)
|
|
57
|
+
VALUES (@slug, @category, @title, @confidence, @created_at, @updated_at, @last_verified_at, @erosion_rate, @token_count, @source_count)
|
|
58
|
+
ON CONFLICT (category, slug) DO UPDATE SET
|
|
59
|
+
title = @title,
|
|
60
|
+
confidence = @confidence,
|
|
61
|
+
updated_at = @updated_at,
|
|
62
|
+
last_verified_at = @last_verified_at,
|
|
63
|
+
erosion_rate = @erosion_rate,
|
|
64
|
+
token_count = @token_count,
|
|
65
|
+
source_count = @source_count
|
|
66
|
+
`);
|
|
67
|
+
stmt.run({
|
|
68
|
+
slug: entry.slug,
|
|
69
|
+
category: entry.category,
|
|
70
|
+
title: entry.title,
|
|
71
|
+
confidence: 1.0,
|
|
72
|
+
created_at: entry.created,
|
|
73
|
+
updated_at: now,
|
|
74
|
+
last_verified_at: now,
|
|
75
|
+
erosion_rate: 0.0,
|
|
76
|
+
token_count: estimateTokens(entry.content),
|
|
77
|
+
source_count: countSources(entry),
|
|
78
|
+
});
|
|
79
|
+
// Sync FTS index: delete old row (if any) then insert fresh content.
|
|
80
|
+
db.prepare("DELETE FROM knowledge_fts WHERE category = ? AND slug = ?").run(entry.category, entry.slug);
|
|
81
|
+
db.prepare(`
|
|
82
|
+
INSERT INTO knowledge_fts (category, slug, title, content, tags_json)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?)
|
|
84
|
+
`).run(entry.category, entry.slug, entry.title, entry.content, JSON.stringify(entry.tags));
|
|
85
|
+
}
|
|
86
|
+
/** Remove metadata for a specific entry. */
|
|
87
|
+
export function removeFromIndex(category, slug) {
|
|
88
|
+
if (!db)
|
|
89
|
+
throw new Error("Metadata DB not initialised — call initMetadataDb first");
|
|
90
|
+
db.prepare("DELETE FROM knowledge_meta WHERE category = ? AND slug = ?").run(category, slug);
|
|
91
|
+
db.prepare("DELETE FROM knowledge_fts WHERE category = ? AND slug = ?").run(category, slug);
|
|
92
|
+
}
|
|
93
|
+
/** Retrieve metadata for a specific entry, or undefined if not found. */
|
|
94
|
+
export function getMetadata(category, slug) {
|
|
95
|
+
if (!db)
|
|
96
|
+
throw new Error("Metadata DB not initialised — call initMetadataDb first");
|
|
97
|
+
const stmt = db.prepare("SELECT * FROM knowledge_meta WHERE category = ? AND slug = ?");
|
|
98
|
+
const row = stmt.get(category, slug);
|
|
99
|
+
return row;
|
|
100
|
+
}
|
|
101
|
+
/** Return all entries with confidence at or below the given threshold. */
|
|
102
|
+
export function getStaleEntries(threshold) {
|
|
103
|
+
if (!db)
|
|
104
|
+
throw new Error("Metadata DB not initialised — call initMetadataDb first");
|
|
105
|
+
const stmt = db.prepare("SELECT * FROM knowledge_meta WHERE confidence <= ? ORDER BY confidence ASC");
|
|
106
|
+
return stmt.all(threshold);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/metadata/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite schema definitions for the metadata store.
|
|
3
|
+
*/
|
|
4
|
+
/** SQL to create the knowledge_meta table. */
|
|
5
|
+
export declare const CREATE_KNOWLEDGE_META = "\nCREATE TABLE IF NOT EXISTS knowledge_meta (\n slug TEXT NOT NULL,\n category TEXT NOT NULL,\n title TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n last_verified_at TEXT NOT NULL,\n erosion_rate REAL NOT NULL DEFAULT 0.0,\n token_count INTEGER NOT NULL DEFAULT 0,\n source_count INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (category, slug)\n);\n";
|
|
6
|
+
/** SQL to create the FTS5 virtual table for full-text search. */
|
|
7
|
+
export declare const CREATE_KNOWLEDGE_FTS = "\nCREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(\n category,\n slug,\n title,\n content,\n tags_json,\n tokenize = 'porter unicode61'\n);\n";
|
|
8
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/metadata/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,8CAA8C;AAC9C,eAAO,MAAM,qBAAqB,sfAcjC,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,oKAShC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite schema definitions for the metadata store.
|
|
3
|
+
*/
|
|
4
|
+
/** SQL to create the knowledge_meta table. */
|
|
5
|
+
export const CREATE_KNOWLEDGE_META = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS knowledge_meta (
|
|
7
|
+
slug TEXT NOT NULL,
|
|
8
|
+
category TEXT NOT NULL,
|
|
9
|
+
title TEXT NOT NULL,
|
|
10
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
11
|
+
created_at TEXT NOT NULL,
|
|
12
|
+
updated_at TEXT NOT NULL,
|
|
13
|
+
last_verified_at TEXT NOT NULL,
|
|
14
|
+
erosion_rate REAL NOT NULL DEFAULT 0.0,
|
|
15
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
source_count INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
PRIMARY KEY (category, slug)
|
|
18
|
+
);
|
|
19
|
+
`;
|
|
20
|
+
/** SQL to create the FTS5 virtual table for full-text search. */
|
|
21
|
+
export const CREATE_KNOWLEDGE_FTS = `
|
|
22
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
|
|
23
|
+
category,
|
|
24
|
+
slug,
|
|
25
|
+
title,
|
|
26
|
+
content,
|
|
27
|
+
tags_json,
|
|
28
|
+
tokenize = 'porter unicode61'
|
|
29
|
+
);
|
|
30
|
+
`;
|
|
31
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/metadata/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,8CAA8C;AAC9C,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;CAcpC,CAAC;AAEF,iEAAiE;AACjE,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;CASnC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-text search over the knowledge base using FTS5 with BM25 ranking.
|
|
3
|
+
*/
|
|
4
|
+
import type { KnowledgeEntry } from "../knowledge-tree/types.js";
|
|
5
|
+
/** A single search result with ranking metadata. */
|
|
6
|
+
export interface SearchResult {
|
|
7
|
+
category: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
title: string;
|
|
10
|
+
snippet: string;
|
|
11
|
+
rank: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Search the knowledge base using FTS5 full-text search with BM25 ranking.
|
|
15
|
+
*
|
|
16
|
+
* @param query - FTS5 query string (supports AND, OR, NOT, prefix*, "phrases")
|
|
17
|
+
* @param limit - Maximum number of results to return (default 10)
|
|
18
|
+
* @returns Ranked search results ordered by relevance (best first)
|
|
19
|
+
*/
|
|
20
|
+
export declare function search(query: string, limit?: number): SearchResult[];
|
|
21
|
+
/**
|
|
22
|
+
* Drop and rebuild the FTS index from a full set of knowledge entries.
|
|
23
|
+
*
|
|
24
|
+
* Useful for reindexing after bulk changes or recovering from corruption.
|
|
25
|
+
*/
|
|
26
|
+
export declare function rebuildIndex(entries: KnowledgeEntry[]): void;
|
|
27
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/metadata/search.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAOjE,oDAAoD;AACpD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAMD;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,YAAY,EAAE,CAkBhE;AAMD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,CAwB5D"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-text search over the knowledge base using FTS5 with BM25 ranking.
|
|
3
|
+
*/
|
|
4
|
+
import { getDb } from "./index.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Search
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Search the knowledge base using FTS5 full-text search with BM25 ranking.
|
|
10
|
+
*
|
|
11
|
+
* @param query - FTS5 query string (supports AND, OR, NOT, prefix*, "phrases")
|
|
12
|
+
* @param limit - Maximum number of results to return (default 10)
|
|
13
|
+
* @returns Ranked search results ordered by relevance (best first)
|
|
14
|
+
*/
|
|
15
|
+
export function search(query, limit = 10) {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
if (!db)
|
|
18
|
+
throw new Error("Metadata DB not initialised — call initMetadataDb first");
|
|
19
|
+
const stmt = db.prepare(`
|
|
20
|
+
SELECT
|
|
21
|
+
category,
|
|
22
|
+
slug,
|
|
23
|
+
title,
|
|
24
|
+
snippet(knowledge_fts, 3, '<mark>', '</mark>', '...', 64) AS snippet,
|
|
25
|
+
bm25(knowledge_fts, 2.0, 1.0, 5.0, 10.0, 1.0) AS rank
|
|
26
|
+
FROM knowledge_fts
|
|
27
|
+
WHERE knowledge_fts MATCH ?
|
|
28
|
+
ORDER BY rank
|
|
29
|
+
LIMIT ?
|
|
30
|
+
`);
|
|
31
|
+
return stmt.all(query, limit);
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Rebuild
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Drop and rebuild the FTS index from a full set of knowledge entries.
|
|
38
|
+
*
|
|
39
|
+
* Useful for reindexing after bulk changes or recovering from corruption.
|
|
40
|
+
*/
|
|
41
|
+
export function rebuildIndex(entries) {
|
|
42
|
+
const db = getDb();
|
|
43
|
+
if (!db)
|
|
44
|
+
throw new Error("Metadata DB not initialised — call initMetadataDb first");
|
|
45
|
+
const dropAndRebuild = db.transaction(() => {
|
|
46
|
+
db.exec("DELETE FROM knowledge_fts");
|
|
47
|
+
const insert = db.prepare(`
|
|
48
|
+
INSERT INTO knowledge_fts (category, slug, title, content, tags_json)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?)
|
|
50
|
+
`);
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
insert.run(entry.category, entry.slug, entry.title, entry.content, JSON.stringify(entry.tags));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
dropAndRebuild();
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/metadata/search.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAenC,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,KAAK,GAAG,EAAE;IAC9C,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAEpF,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;;;;;;GAWvB,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAmB,CAAC;AAClD,CAAC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAAyB;IACpD,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAEpF,MAAM,cAAc,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QACzC,EAAE,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QAErC,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC;;;KAGzB,CAAC,CAAC;QAEH,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,CACR,KAAK,CAAC,QAAQ,EACd,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,OAAO,EACb,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAC3B,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,cAAc,EAAE,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata store type definitions.
|
|
3
|
+
*/
|
|
4
|
+
/** A row in the knowledge_meta table. */
|
|
5
|
+
export interface KnowledgeMetadata {
|
|
6
|
+
slug: string;
|
|
7
|
+
category: string;
|
|
8
|
+
title: string;
|
|
9
|
+
confidence: number;
|
|
10
|
+
created_at: string;
|
|
11
|
+
updated_at: string;
|
|
12
|
+
last_verified_at: string;
|
|
13
|
+
erosion_rate: number;
|
|
14
|
+
token_count: number;
|
|
15
|
+
source_count: number;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/metadata/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/metadata/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.test.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/__tests__/orchestrator.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { initMetadataDb, closeDb } from "../../metadata/index.js";
|
|
6
|
+
import { initCuriosityQueue, addCuriosity, fetchPending } from "../../curiosity/index.js";
|
|
7
|
+
import { initKnowledgeTree, writeEntry } from "../../knowledge-tree/index.js";
|
|
8
|
+
import { initBudgetTables, setBudgetConfig, charge } from "../../budget/index.js";
|
|
9
|
+
import { setConfidenceConfig } from "../../confidence/index.js";
|
|
10
|
+
import { runOrchestrator, getRecentRuns } from "../index.js";
|
|
11
|
+
import { matchesCron, startScheduler, stopScheduler, isSchedulerRunning } from "../scheduler.js";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Mock providers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
class MockSearchProvider {
|
|
16
|
+
name = "mock-search";
|
|
17
|
+
_lastCost = 0;
|
|
18
|
+
callCount = 0;
|
|
19
|
+
async search() {
|
|
20
|
+
this.callCount++;
|
|
21
|
+
this._lastCost = 0.01;
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
title: "Result for query",
|
|
25
|
+
url: "https://example.com/result",
|
|
26
|
+
snippet: "A detailed search result with enough content to pass vetting.",
|
|
27
|
+
content: "Comprehensive content about the topic with sufficient detail for synthesis.",
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
lastCallCostUsd() {
|
|
32
|
+
return this._lastCost;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class MockModelProvider {
|
|
36
|
+
name = "mock-model";
|
|
37
|
+
_lastCost = 0;
|
|
38
|
+
callCount = 0;
|
|
39
|
+
async synthesize(question, searchResults) {
|
|
40
|
+
this.callCount++;
|
|
41
|
+
this._lastCost = 0.005;
|
|
42
|
+
return {
|
|
43
|
+
title: `Synthesised: ${question.slice(0, 50)}`,
|
|
44
|
+
category: "technology",
|
|
45
|
+
content: `Answer synthesised from ${searchResults.length} sources about ${question}.`,
|
|
46
|
+
tags: ["research"],
|
|
47
|
+
source: searchResults[0]?.url ?? "unknown",
|
|
48
|
+
confidence: 0.85,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async categorize(_content, categories) {
|
|
52
|
+
this._lastCost = 0.001;
|
|
53
|
+
return categories[0] ?? "general";
|
|
54
|
+
}
|
|
55
|
+
lastCallCostUsd() {
|
|
56
|
+
return this._lastCost;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Test setup
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
let tmpDir;
|
|
63
|
+
let dataDir;
|
|
64
|
+
let db;
|
|
65
|
+
beforeEach(async () => {
|
|
66
|
+
tmpDir = await mkdtemp(join(tmpdir(), "cq-orchestrator-test-"));
|
|
67
|
+
dataDir = join(tmpDir, "knowledge");
|
|
68
|
+
await initKnowledgeTree(dataDir);
|
|
69
|
+
db = initMetadataDb(":memory:");
|
|
70
|
+
initCuriosityQueue(db);
|
|
71
|
+
initBudgetTables();
|
|
72
|
+
setBudgetConfig({ dailyLimitUsd: 5.0 });
|
|
73
|
+
});
|
|
74
|
+
afterEach(async () => {
|
|
75
|
+
stopScheduler();
|
|
76
|
+
closeDb();
|
|
77
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
function makeOptions(overrides = {}) {
|
|
80
|
+
return {
|
|
81
|
+
db,
|
|
82
|
+
dataDir,
|
|
83
|
+
searchProvider: new MockSearchProvider(),
|
|
84
|
+
modelProvider: new MockModelProvider(),
|
|
85
|
+
...overrides,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const DISTINCT_QUESTIONS = [
|
|
89
|
+
"What is quantum computing and how does it work?",
|
|
90
|
+
"How do black holes form in outer space?",
|
|
91
|
+
"What causes the Northern Lights aurora borealis?",
|
|
92
|
+
"How does CRISPR gene editing technology function?",
|
|
93
|
+
"What is the theory of general relativity?",
|
|
94
|
+
];
|
|
95
|
+
function addTestItems(count) {
|
|
96
|
+
for (let i = 0; i < count; i++) {
|
|
97
|
+
addCuriosity({
|
|
98
|
+
question: DISTINCT_QUESTIONS[i % DISTINCT_QUESTIONS.length],
|
|
99
|
+
context: `Test context ${i}`,
|
|
100
|
+
source: "agent",
|
|
101
|
+
priority: 10 - i,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// runOrchestrator — two-phase execution
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
describe("runOrchestrator — Phase 1 (Curiosity)", () => {
|
|
109
|
+
it("processes pending curiosity items", async () => {
|
|
110
|
+
addTestItems(3);
|
|
111
|
+
const result = await runOrchestrator(makeOptions());
|
|
112
|
+
expect(result.status).toBe("completed");
|
|
113
|
+
expect(result.phases.curiosity.processed).toBe(3);
|
|
114
|
+
expect(result.phases.curiosity.succeeded).toBe(3);
|
|
115
|
+
expect(result.phases.curiosity.failed).toBe(0);
|
|
116
|
+
expect(result.totalDurationMs).toBeGreaterThanOrEqual(0);
|
|
117
|
+
expect(result.runDate).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
it("respects maxCuriosityItems limit", async () => {
|
|
120
|
+
addTestItems(5);
|
|
121
|
+
const result = await runOrchestrator(makeOptions({ maxCuriosityItems: 2 }));
|
|
122
|
+
expect(result.phases.curiosity.processed).toBe(2);
|
|
123
|
+
// Should still have 3 pending
|
|
124
|
+
const remaining = fetchPending(10);
|
|
125
|
+
expect(remaining.length).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
it("returns completed with zero items when queue is empty", async () => {
|
|
128
|
+
const result = await runOrchestrator(makeOptions());
|
|
129
|
+
expect(result.status).toBe("completed");
|
|
130
|
+
expect(result.phases.curiosity.processed).toBe(0);
|
|
131
|
+
expect(result.phases.curiosity.succeeded).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
it("tracks cost across items", async () => {
|
|
134
|
+
addTestItems(2);
|
|
135
|
+
const result = await runOrchestrator(makeOptions());
|
|
136
|
+
expect(result.phases.curiosity.costUsd).toBeGreaterThan(0);
|
|
137
|
+
expect(result.totalBudgetUsedUsd).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Budget exhaustion
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
describe("runOrchestrator — budget exhaustion", () => {
|
|
144
|
+
it("stops processing when budget is exhausted", async () => {
|
|
145
|
+
// Set budget so low that only 1–2 items can run (each costs ~$0.015)
|
|
146
|
+
setBudgetConfig({ dailyLimitUsd: 0.02 });
|
|
147
|
+
addTestItems(5);
|
|
148
|
+
const result = await runOrchestrator(makeOptions());
|
|
149
|
+
// Should process fewer than all 5
|
|
150
|
+
const total = result.phases.curiosity.processed + result.phases.curiosity.skipped;
|
|
151
|
+
expect(total).toBeLessThanOrEqual(5);
|
|
152
|
+
expect(result.phases.curiosity.processed).toBeLessThan(5);
|
|
153
|
+
});
|
|
154
|
+
it("skips Phase 2 when budget below proactive minimum", async () => {
|
|
155
|
+
// Set budget just enough for Phase 1 but not Phase 2
|
|
156
|
+
setBudgetConfig({ dailyLimitUsd: 0.05 });
|
|
157
|
+
addTestItems(1);
|
|
158
|
+
const result = await runOrchestrator(makeOptions({ proactiveMinBudgetUsd: 10.0 }));
|
|
159
|
+
expect(result.phases.proactive).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Phase 2 (Proactive)
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
describe("runOrchestrator — Phase 2 (Proactive)", () => {
|
|
166
|
+
it("refreshes low-confidence entries", async () => {
|
|
167
|
+
// Create a knowledge entry and manually set its confidence low
|
|
168
|
+
await writeEntry(dataDir, {
|
|
169
|
+
category: "technology",
|
|
170
|
+
title: "Outdated knowledge",
|
|
171
|
+
content: "This is old content that needs refreshing.",
|
|
172
|
+
tags: ["old"],
|
|
173
|
+
});
|
|
174
|
+
// Index it with low confidence
|
|
175
|
+
db.prepare(`
|
|
176
|
+
INSERT INTO knowledge_meta (slug, category, title, confidence, created_at, updated_at, last_verified_at, erosion_rate, token_count, source_count)
|
|
177
|
+
VALUES ('outdated-knowledge', 'technology', 'Outdated knowledge', 0.1, '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z', 0.02, 100, 1)
|
|
178
|
+
`).run();
|
|
179
|
+
setConfidenceConfig({ refresh_threshold: 0.4, default_rate: 0.02 });
|
|
180
|
+
const result = await runOrchestrator(makeOptions());
|
|
181
|
+
// Phase 2 should have run
|
|
182
|
+
expect(result.phases.proactive).not.toBeNull();
|
|
183
|
+
expect(result.phases.proactive.refreshed).toBeGreaterThanOrEqual(0);
|
|
184
|
+
});
|
|
185
|
+
it("fills thin categories", async () => {
|
|
186
|
+
// Create a category with only 1 entry (below default threshold of 3)
|
|
187
|
+
await writeEntry(dataDir, {
|
|
188
|
+
category: "sparse-topic",
|
|
189
|
+
title: "Only entry",
|
|
190
|
+
content: "This is the only entry in this thin category.",
|
|
191
|
+
tags: [],
|
|
192
|
+
});
|
|
193
|
+
const result = await runOrchestrator(makeOptions({ thinCategoryThreshold: 3 }));
|
|
194
|
+
expect(result.phases.proactive).not.toBeNull();
|
|
195
|
+
expect(result.phases.proactive.thinCategoryFilled).toBeGreaterThanOrEqual(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Run log
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
describe("research_job_log", () => {
|
|
202
|
+
it("records run history", async () => {
|
|
203
|
+
addTestItems(2);
|
|
204
|
+
await runOrchestrator(makeOptions());
|
|
205
|
+
const runs = getRecentRuns(db, 5);
|
|
206
|
+
expect(runs.length).toBe(1);
|
|
207
|
+
expect(runs[0].status).toBe("completed");
|
|
208
|
+
expect(runs[0].phase1_processed).toBe(2);
|
|
209
|
+
expect(runs[0].total_cost_usd).toBeGreaterThan(0);
|
|
210
|
+
expect(runs[0].duration_ms).toBeGreaterThanOrEqual(0);
|
|
211
|
+
expect(runs[0].run_date).toBeDefined();
|
|
212
|
+
expect(runs[0].created_at).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
it("records multiple runs in order", async () => {
|
|
215
|
+
addTestItems(1);
|
|
216
|
+
await runOrchestrator(makeOptions());
|
|
217
|
+
addTestItems(1);
|
|
218
|
+
await runOrchestrator(makeOptions());
|
|
219
|
+
const runs = getRecentRuns(db, 10);
|
|
220
|
+
expect(runs.length).toBe(2);
|
|
221
|
+
// Most recent first
|
|
222
|
+
expect(runs[0].created_at >= runs[1].created_at).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
it("respects limit parameter", async () => {
|
|
225
|
+
addTestItems(1);
|
|
226
|
+
await runOrchestrator(makeOptions());
|
|
227
|
+
addTestItems(1);
|
|
228
|
+
await runOrchestrator(makeOptions());
|
|
229
|
+
addTestItems(1);
|
|
230
|
+
await runOrchestrator(makeOptions());
|
|
231
|
+
const runs = getRecentRuns(db, 2);
|
|
232
|
+
expect(runs.length).toBe(2);
|
|
233
|
+
});
|
|
234
|
+
it("records error for failed runs", async () => {
|
|
235
|
+
// Pre-exhaust budget by charging more than the limit
|
|
236
|
+
setBudgetConfig({ dailyLimitUsd: 1.0 });
|
|
237
|
+
charge("other", 1.5);
|
|
238
|
+
addTestItems(3);
|
|
239
|
+
await runOrchestrator(makeOptions());
|
|
240
|
+
const runs = getRecentRuns(db, 1);
|
|
241
|
+
expect(runs.length).toBe(1);
|
|
242
|
+
// All items should be skipped since budget is already over limit
|
|
243
|
+
expect(runs[0].phase1_processed).toBe(0);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// OrchestratorResult shape
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
describe("OrchestratorResult shape", () => {
|
|
250
|
+
it("has all required fields", async () => {
|
|
251
|
+
addTestItems(1);
|
|
252
|
+
const result = await runOrchestrator(makeOptions());
|
|
253
|
+
expect(result).toHaveProperty("runDate");
|
|
254
|
+
expect(result).toHaveProperty("status");
|
|
255
|
+
expect(result).toHaveProperty("phases");
|
|
256
|
+
expect(result).toHaveProperty("phases.curiosity");
|
|
257
|
+
expect(result).toHaveProperty("totalDurationMs");
|
|
258
|
+
expect(result).toHaveProperty("totalBudgetUsedUsd");
|
|
259
|
+
expect(result.phases.curiosity).toHaveProperty("processed");
|
|
260
|
+
expect(result.phases.curiosity).toHaveProperty("succeeded");
|
|
261
|
+
expect(result.phases.curiosity).toHaveProperty("failed");
|
|
262
|
+
expect(result.phases.curiosity).toHaveProperty("skipped");
|
|
263
|
+
expect(result.phases.curiosity).toHaveProperty("costUsd");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// matchesCron
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
describe("matchesCron", () => {
|
|
270
|
+
it("matches wildcard pattern", () => {
|
|
271
|
+
const date = new Date(2025, 5, 15, 10, 30); // June 15, 10:30
|
|
272
|
+
expect(matchesCron("* * *", date)).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
it("matches exact time", () => {
|
|
275
|
+
const date = new Date(2025, 0, 1, 3, 0); // Jan 1, 3:00 AM
|
|
276
|
+
expect(matchesCron("0 3 *", date)).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
it("rejects non-matching minute", () => {
|
|
279
|
+
const date = new Date(2025, 0, 1, 3, 15);
|
|
280
|
+
expect(matchesCron("0 3 *", date)).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
it("rejects non-matching hour", () => {
|
|
283
|
+
const date = new Date(2025, 0, 1, 5, 0);
|
|
284
|
+
expect(matchesCron("0 3 *", date)).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
it("matches specific day of month", () => {
|
|
287
|
+
const date = new Date(2025, 0, 15, 0, 0);
|
|
288
|
+
expect(matchesCron("0 0 15", date)).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
it("rejects non-matching day of month", () => {
|
|
291
|
+
const date = new Date(2025, 0, 14, 0, 0);
|
|
292
|
+
expect(matchesCron("0 0 15", date)).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
it("supports comma-separated values", () => {
|
|
295
|
+
const date6am = new Date(2025, 0, 1, 6, 0);
|
|
296
|
+
const date18pm = new Date(2025, 0, 1, 18, 0);
|
|
297
|
+
const date12pm = new Date(2025, 0, 1, 12, 0);
|
|
298
|
+
expect(matchesCron("0 6,18 *", date6am)).toBe(true);
|
|
299
|
+
expect(matchesCron("0 6,18 *", date18pm)).toBe(true);
|
|
300
|
+
expect(matchesCron("0 6,18 *", date12pm)).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
it("throws on invalid cron expression", () => {
|
|
303
|
+
expect(() => matchesCron("0 3", new Date())).toThrow("Invalid cron expression");
|
|
304
|
+
expect(() => matchesCron("0 3 * *", new Date())).toThrow("Invalid cron expression");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Scheduler
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
describe("scheduler", () => {
|
|
311
|
+
afterEach(() => {
|
|
312
|
+
stopScheduler();
|
|
313
|
+
});
|
|
314
|
+
it("starts and reports running state", () => {
|
|
315
|
+
expect(isSchedulerRunning()).toBe(false);
|
|
316
|
+
startScheduler({
|
|
317
|
+
cron: "0 3 *",
|
|
318
|
+
orchestratorOptions: makeOptions(),
|
|
319
|
+
});
|
|
320
|
+
expect(isSchedulerRunning()).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
it("stops and reports not running", () => {
|
|
323
|
+
startScheduler({
|
|
324
|
+
cron: "0 3 *",
|
|
325
|
+
orchestratorOptions: makeOptions(),
|
|
326
|
+
});
|
|
327
|
+
stopScheduler();
|
|
328
|
+
expect(isSchedulerRunning()).toBe(false);
|
|
329
|
+
});
|
|
330
|
+
it("throws when starting an already-running scheduler", () => {
|
|
331
|
+
startScheduler({
|
|
332
|
+
cron: "0 3 *",
|
|
333
|
+
orchestratorOptions: makeOptions(),
|
|
334
|
+
});
|
|
335
|
+
expect(() => startScheduler({
|
|
336
|
+
cron: "0 3 *",
|
|
337
|
+
orchestratorOptions: makeOptions(),
|
|
338
|
+
})).toThrow("already running");
|
|
339
|
+
});
|
|
340
|
+
it("can be restarted after stopping", () => {
|
|
341
|
+
startScheduler({
|
|
342
|
+
cron: "0 3 *",
|
|
343
|
+
orchestratorOptions: makeOptions(),
|
|
344
|
+
});
|
|
345
|
+
stopScheduler();
|
|
346
|
+
// Should not throw
|
|
347
|
+
startScheduler({
|
|
348
|
+
cron: "0 3 *",
|
|
349
|
+
orchestratorOptions: makeOptions(),
|
|
350
|
+
});
|
|
351
|
+
expect(isSchedulerRunning()).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
//# sourceMappingURL=orchestrator.test.js.map
|