@balpal4495/quorum 3.0.4 → 3.1.0
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/advisor/ask.d.ts +13 -0
- package/dist/advisor/ask.d.ts.map +1 -0
- package/dist/advisor/ask.js +67 -0
- package/dist/advisor/ask.js.map +1 -0
- package/dist/advisor/index.d.ts +3 -0
- package/dist/advisor/index.d.ts.map +1 -0
- package/dist/advisor/index.js +2 -0
- package/dist/advisor/index.js.map +1 -0
- package/dist/advisor/prompt.d.ts +5 -0
- package/dist/advisor/prompt.d.ts.map +1 -0
- package/{modules/advisor/prompt.ts → dist/advisor/prompt.js} +22 -26
- package/dist/advisor/prompt.js.map +1 -0
- package/dist/advisor/types.d.ts +23 -0
- package/dist/advisor/types.d.ts.map +1 -0
- package/dist/advisor/types.js +2 -0
- package/dist/advisor/types.js.map +1 -0
- package/dist/compass/behavior.d.ts +4 -0
- package/dist/compass/behavior.d.ts.map +1 -0
- package/dist/compass/behavior.js +138 -0
- package/dist/compass/behavior.js.map +1 -0
- package/dist/compass/create.d.ts +3 -0
- package/dist/compass/create.d.ts.map +1 -0
- package/dist/compass/create.js +289 -0
- package/dist/compass/create.js.map +1 -0
- package/dist/compass/evidence/collect.d.ts +11 -0
- package/dist/compass/evidence/collect.d.ts.map +1 -0
- package/dist/compass/evidence/collect.js +86 -0
- package/dist/compass/evidence/collect.js.map +1 -0
- package/dist/compass/index.d.ts +8 -0
- package/dist/compass/index.d.ts.map +1 -0
- package/dist/compass/index.js +8 -0
- package/dist/compass/index.js.map +1 -0
- package/dist/compass/prompts/index.d.ts +28 -0
- package/dist/compass/prompts/index.d.ts.map +1 -0
- package/{modules/compass/prompts/index.ts → dist/compass/prompts/index.js} +13 -38
- package/dist/compass/prompts/index.js.map +1 -0
- package/dist/compass/prompts/system.d.ts +2 -0
- package/dist/compass/prompts/system.d.ts.map +1 -0
- package/{modules/compass/prompts/system.ts → dist/compass/prompts/system.js} +2 -1
- package/dist/compass/prompts/system.js.map +1 -0
- package/dist/compass/propose.d.ts +15 -0
- package/dist/compass/propose.d.ts.map +1 -0
- package/dist/compass/propose.js +128 -0
- package/dist/compass/propose.js.map +1 -0
- package/dist/compass/schemas.d.ts +1271 -0
- package/dist/compass/schemas.d.ts.map +1 -0
- package/dist/compass/schemas.js +113 -0
- package/dist/compass/schemas.js.map +1 -0
- package/dist/compass/score.d.ts +25 -0
- package/dist/compass/score.d.ts.map +1 -0
- package/dist/compass/score.js +89 -0
- package/dist/compass/score.js.map +1 -0
- package/dist/compass/sources/index.d.ts +9 -0
- package/dist/compass/sources/index.d.ts.map +1 -0
- package/dist/compass/sources/index.js +408 -0
- package/dist/compass/sources/index.js.map +1 -0
- package/dist/compass/types.d.ts +334 -0
- package/dist/compass/types.d.ts.map +1 -0
- package/dist/compass/types.js +2 -0
- package/dist/compass/types.js.map +1 -0
- package/dist/council/advisors.d.ts +15 -0
- package/dist/council/advisors.d.ts.map +1 -0
- package/dist/council/advisors.js +46 -0
- package/dist/council/advisors.js.map +1 -0
- package/dist/council/chairman.d.ts +13 -0
- package/dist/council/chairman.d.ts.map +1 -0
- package/dist/council/chairman.js +145 -0
- package/dist/council/chairman.js.map +1 -0
- package/dist/council/deliberate.d.ts +22 -0
- package/dist/council/deliberate.d.ts.map +1 -0
- package/dist/council/deliberate.js +99 -0
- package/dist/council/deliberate.js.map +1 -0
- package/dist/council/frame.d.ts +8 -0
- package/dist/council/frame.d.ts.map +1 -0
- package/dist/council/frame.js +40 -0
- package/dist/council/frame.js.map +1 -0
- package/dist/council/index.d.ts +6 -0
- package/dist/council/index.d.ts.map +1 -0
- package/dist/council/index.js +4 -0
- package/dist/council/index.js.map +1 -0
- package/dist/council/personas.d.ts +18 -0
- package/dist/council/personas.d.ts.map +1 -0
- package/dist/council/personas.js +44 -0
- package/dist/council/personas.js.map +1 -0
- package/dist/council/reviewers.d.ts +13 -0
- package/dist/council/reviewers.d.ts.map +1 -0
- package/dist/council/reviewers.js +59 -0
- package/dist/council/reviewers.js.map +1 -0
- package/dist/council/risk.d.ts +16 -0
- package/dist/council/risk.d.ts.map +1 -0
- package/dist/council/risk.js +74 -0
- package/dist/council/risk.js.map +1 -0
- package/dist/council/types.d.ts +95 -0
- package/dist/council/types.d.ts.map +1 -0
- package/dist/council/types.js +2 -0
- package/dist/council/types.js.map +1 -0
- package/dist/jury/evaluate.d.ts +13 -0
- package/dist/jury/evaluate.d.ts.map +1 -0
- package/{modules/jury/evaluate.ts → dist/jury/evaluate.js} +60 -84
- package/dist/jury/evaluate.js.map +1 -0
- package/dist/jury/index.d.ts +6 -0
- package/dist/jury/index.d.ts.map +1 -0
- package/dist/jury/index.js +4 -0
- package/dist/jury/index.js.map +1 -0
- package/dist/jury/preflight.d.ts +26 -0
- package/dist/jury/preflight.d.ts.map +1 -0
- package/dist/jury/preflight.js +71 -0
- package/dist/jury/preflight.js.map +1 -0
- package/dist/jury/schema.d.ts +57 -0
- package/dist/jury/schema.d.ts.map +1 -0
- package/dist/jury/schema.js +21 -0
- package/dist/jury/schema.js.map +1 -0
- package/dist/jury/types.d.ts +47 -0
- package/dist/jury/types.d.ts.map +1 -0
- package/dist/jury/types.js +2 -0
- package/dist/jury/types.js.map +1 -0
- package/dist/oracle/adapters/lance-db.d.ts +15 -0
- package/dist/oracle/adapters/lance-db.d.ts.map +1 -0
- package/dist/oracle/adapters/lance-db.js +68 -0
- package/dist/oracle/adapters/lance-db.js.map +1 -0
- package/dist/oracle/adapters/xenova-embedder.d.ts +21 -0
- package/dist/oracle/adapters/xenova-embedder.d.ts.map +1 -0
- package/dist/oracle/adapters/xenova-embedder.js +36 -0
- package/dist/oracle/adapters/xenova-embedder.js.map +1 -0
- package/dist/oracle/bm25.d.ts +20 -0
- package/dist/oracle/bm25.d.ts.map +1 -0
- package/dist/oracle/bm25.js +82 -0
- package/dist/oracle/bm25.js.map +1 -0
- package/dist/oracle/index.d.ts +21 -0
- package/dist/oracle/index.d.ts.map +1 -0
- package/dist/oracle/index.js +25 -0
- package/dist/oracle/index.js.map +1 -0
- package/dist/oracle/log.d.ts +6 -0
- package/dist/oracle/log.d.ts.map +1 -0
- package/dist/oracle/log.js +12 -0
- package/dist/oracle/log.js.map +1 -0
- package/dist/oracle/propose.d.ts +25 -0
- package/dist/oracle/propose.d.ts.map +1 -0
- package/dist/oracle/propose.js +133 -0
- package/dist/oracle/propose.js.map +1 -0
- package/dist/oracle/query.d.ts +17 -0
- package/dist/oracle/query.d.ts.map +1 -0
- package/dist/oracle/query.js +106 -0
- package/dist/oracle/query.js.map +1 -0
- package/dist/oracle/summary.d.ts +11 -0
- package/dist/oracle/summary.d.ts.map +1 -0
- package/dist/oracle/summary.js +102 -0
- package/dist/oracle/summary.js.map +1 -0
- package/dist/oracle/types.d.ts +31 -0
- package/dist/oracle/types.d.ts.map +1 -0
- package/dist/oracle/types.js +2 -0
- package/dist/oracle/types.js.map +1 -0
- package/dist/sentinel/assert.d.ts +28 -0
- package/dist/sentinel/assert.d.ts.map +1 -0
- package/dist/sentinel/assert.js +63 -0
- package/dist/sentinel/assert.js.map +1 -0
- package/dist/sentinel/coverage.d.ts +14 -0
- package/dist/sentinel/coverage.d.ts.map +1 -0
- package/dist/sentinel/coverage.js +96 -0
- package/dist/sentinel/coverage.js.map +1 -0
- package/dist/sentinel/drift.d.ts +12 -0
- package/dist/sentinel/drift.d.ts.map +1 -0
- package/dist/sentinel/drift.js +149 -0
- package/dist/sentinel/drift.js.map +1 -0
- package/dist/sentinel/index.d.ts +7 -0
- package/dist/sentinel/index.d.ts.map +1 -0
- package/dist/sentinel/index.js +5 -0
- package/dist/sentinel/index.js.map +1 -0
- package/dist/sentinel/review.d.ts +15 -0
- package/dist/sentinel/review.d.ts.map +1 -0
- package/dist/sentinel/review.js +177 -0
- package/dist/sentinel/review.js.map +1 -0
- package/dist/setup.d.ts +103 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +87 -0
- package/dist/setup.js.map +1 -0
- package/dist/shared/types.d.ts +173 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +16 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +13 -8
- package/.github/copilot-instructions.md +0 -117
- package/CLAUDE.md +0 -146
- package/GEMINI.md +0 -73
- package/SETUP.md +0 -264
- package/evals/__tests__/eval.test.ts +0 -31
- package/evals/cases/auth_hs256_rejected.json +0 -46
- package/evals/cases/auth_rs256_valid.json +0 -30
- package/evals/cases/cache_missing_lock.json +0 -31
- package/evals/cases/db_naive_not_null.json +0 -32
- package/evals/cases/logging_pii_leak.json +0 -32
- package/evals/cases/migration_with_rollback.json +0 -43
- package/evals/cases/no_evidence_novel_design.json +0 -16
- package/evals/cases/payment_no_idempotency.json +0 -33
- package/evals/cases/redis_session_rejected.json +0 -32
- package/evals/cases/safe_refactor.json +0 -17
- package/evals/runner.ts +0 -226
- package/modules/AGENTS.md +0 -78
- package/modules/CLAUDE.md +0 -93
- package/modules/README.md +0 -504
- package/modules/advisor/ask.ts +0 -87
- package/modules/advisor/index.ts +0 -2
- package/modules/advisor/types.ts +0 -26
- package/modules/compass/behavior.ts +0 -161
- package/modules/compass/create.ts +0 -365
- package/modules/compass/evidence/collect.ts +0 -109
- package/modules/compass/index.ts +0 -7
- package/modules/compass/propose.ts +0 -152
- package/modules/compass/schemas.ts +0 -121
- package/modules/compass/score.ts +0 -77
- package/modules/compass/sources/index.ts +0 -413
- package/modules/compass/types.ts +0 -431
- package/modules/council/advisors.ts +0 -71
- package/modules/council/chairman.ts +0 -183
- package/modules/council/deliberate.ts +0 -141
- package/modules/council/frame.ts +0 -54
- package/modules/council/index.ts +0 -9
- package/modules/council/personas.ts +0 -57
- package/modules/council/reviewers.ts +0 -82
- package/modules/council/risk.ts +0 -89
- package/modules/council/types.ts +0 -107
- package/modules/jury/index.ts +0 -5
- package/modules/jury/preflight.ts +0 -101
- package/modules/jury/schema.ts +0 -24
- package/modules/jury/types.ts +0 -50
- package/modules/oracle/adapters/lance-db.ts +0 -81
- package/modules/oracle/adapters/xenova-embedder.ts +0 -43
- package/modules/oracle/bm25.ts +0 -92
- package/modules/oracle/index.ts +0 -36
- package/modules/oracle/log.ts +0 -15
- package/modules/oracle/propose.ts +0 -164
- package/modules/oracle/query.ts +0 -146
- package/modules/oracle/summary.ts +0 -116
- package/modules/oracle/types.ts +0 -32
- package/modules/sentinel/assert.ts +0 -95
- package/modules/sentinel/coverage.ts +0 -106
- package/modules/sentinel/drift.ts +0 -163
- package/modules/sentinel/index.ts +0 -6
- package/modules/sentinel/review.ts +0 -208
- package/modules/setup.ts +0 -202
- package/modules/shared/types.ts +0 -193
package/modules/jury/types.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { OracleResult, LLMProvider } from "../shared/types"
|
|
2
|
-
|
|
3
|
-
export interface JuryInput {
|
|
4
|
-
/** What needs to be achieved. */
|
|
5
|
-
outcome: string
|
|
6
|
-
/** Proposed approach from the Designer. */
|
|
7
|
-
design: string
|
|
8
|
-
/** Evidence retrieved from Oracle. */
|
|
9
|
-
evidence: OracleResult[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Per-dimension breakdown of the 0–1 confidence score. */
|
|
13
|
-
export interface ConfidenceBreakdown {
|
|
14
|
-
/** Do validated Oracle entries confirm this approach works here? */
|
|
15
|
-
evidence_support: number
|
|
16
|
-
/** Do Oracle entries suggest this is achievable in this codebase? */
|
|
17
|
-
feasibility: number
|
|
18
|
-
/** How well does the design address known failure modes? (1 = fully addressed) */
|
|
19
|
-
risk: number
|
|
20
|
-
/** Does the design cover the full outcome, or only part of it? */
|
|
21
|
-
completeness: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface JuryOutput {
|
|
25
|
-
/** 0–1 confidence score. Average of the four breakdown dimensions. */
|
|
26
|
-
confidence: number
|
|
27
|
-
/** Per-dimension breakdown of the confidence score. */
|
|
28
|
-
confidence_breakdown: ConfidenceBreakdown
|
|
29
|
-
/** What the evidence supports or contradicts. */
|
|
30
|
-
assessment: string
|
|
31
|
-
/** Evidence missing from Oracle that would improve confidence. */
|
|
32
|
-
gaps: string[]
|
|
33
|
-
/**
|
|
34
|
-
* Gaps that are hard blockers — must be resolved before Council should proceed.
|
|
35
|
-
* Subset of gaps where the missing information is critical (auth, rollback, data safety).
|
|
36
|
-
*/
|
|
37
|
-
blocking_gaps: string[]
|
|
38
|
-
/**
|
|
39
|
-
* Council brief derived from confidence:
|
|
40
|
-
* < 0.6 → "challenge" (find what is wrong — broader scope)
|
|
41
|
-
* ≥ 0.6 → "pressure-test" (assume correct, try to break it)
|
|
42
|
-
*/
|
|
43
|
-
council_brief: "challenge" | "pressure-test"
|
|
44
|
-
recommendation: "proceed" | "investigate-more" | "redesign"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface JuryDeps {
|
|
48
|
-
llm: LLMProvider
|
|
49
|
-
model?: string
|
|
50
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LanceDB vector store adapter.
|
|
3
|
-
*
|
|
4
|
-
* Required package: npm install vectordb
|
|
5
|
-
*
|
|
6
|
-
* Chronicle entries are stored in .chronicle/entries/ (LanceDB table directory).
|
|
7
|
-
* Vectors are indexed with cosine metric — no need to pre-normalise embeddings.
|
|
8
|
-
*
|
|
9
|
-
* Note: this adapter targets the `vectordb` package (LanceDB v0.x).
|
|
10
|
-
* If your project uses `@lancedb/lancedb` (v0.4+), the connect/createTable API
|
|
11
|
-
* is nearly identical but table.query() replaces table.search() for non-vector queries.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { VectorStore } from "../types"
|
|
15
|
-
import type { ChronicleEntry } from "../../shared/types"
|
|
16
|
-
import path from "path"
|
|
17
|
-
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
|
-
const lancedb = require("vectordb")
|
|
20
|
-
|
|
21
|
-
interface LanceRow {
|
|
22
|
-
id: string
|
|
23
|
-
vector: number[]
|
|
24
|
-
/** ChronicleEntry serialised as JSON string. */
|
|
25
|
-
payload: string
|
|
26
|
-
_distance?: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function createLanceDBStore(chronicleDir: string): Promise<VectorStore> {
|
|
30
|
-
const tableDir = path.join(chronicleDir, "entries")
|
|
31
|
-
const db = await lancedb.connect(tableDir)
|
|
32
|
-
let table: any = null
|
|
33
|
-
|
|
34
|
-
async function getOrCreateTable(firstRow?: LanceRow): Promise<any> {
|
|
35
|
-
if (table) return table
|
|
36
|
-
const names: string[] = await db.tableNames()
|
|
37
|
-
if (names.includes("entries")) {
|
|
38
|
-
table = await db.openTable("entries")
|
|
39
|
-
} else if (firstRow) {
|
|
40
|
-
table = await db.createTable("entries", [firstRow], { metric: "cosine" })
|
|
41
|
-
}
|
|
42
|
-
return table
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
async upsert(id, vector, metadata) {
|
|
47
|
-
const row: LanceRow = { id, vector, payload: JSON.stringify(metadata) }
|
|
48
|
-
const t = await getOrCreateTable(row)
|
|
49
|
-
if (t !== table) {
|
|
50
|
-
// table was just created with this row — already inserted
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
// LanceDB does not have native upsert — delete existing then insert
|
|
54
|
-
await t.delete(`id = '${sanitiseId(id)}'`)
|
|
55
|
-
await t.add([row])
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
async search(vector, limit) {
|
|
59
|
-
const t = await getOrCreateTable()
|
|
60
|
-
if (!t) return []
|
|
61
|
-
const rows: LanceRow[] = await t.search(vector).limit(limit).execute()
|
|
62
|
-
return rows.map(row => ({
|
|
63
|
-
entry: JSON.parse(row.payload) as ChronicleEntry,
|
|
64
|
-
// Convert L2 distance (cosine metric stores 1 - cosine_sim as distance)
|
|
65
|
-
score: row._distance !== undefined ? 1 - row._distance : 0,
|
|
66
|
-
}))
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
async getAll() {
|
|
70
|
-
const t = await getOrCreateTable()
|
|
71
|
-
if (!t) return []
|
|
72
|
-
const rows: LanceRow[] = await t.query().execute()
|
|
73
|
-
return rows.map(row => JSON.parse(row.payload) as ChronicleEntry)
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Prevent SQL injection in the delete filter. LanceDB uses SQL-like WHERE clauses. */
|
|
79
|
-
function sanitiseId(id: string): string {
|
|
80
|
-
return id.replace(/'/g, "''")
|
|
81
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local ONNX embedder using @xenova/transformers (all-MiniLM-L6-v2).
|
|
3
|
-
*
|
|
4
|
-
* Required package: npm install @xenova/transformers
|
|
5
|
-
*
|
|
6
|
-
* Runs entirely locally — no API key, no network dependency after first use.
|
|
7
|
-
* First call downloads and caches the model (~25 MB).
|
|
8
|
-
* Produces 384-dimensional unit vectors (mean pooling + L2 normalisation).
|
|
9
|
-
*
|
|
10
|
-
* For production use, pre-warm the embedder on startup:
|
|
11
|
-
* import { warmEmbedder } from "./adapters/xenova-embedder"
|
|
12
|
-
* await warmEmbedder()
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
-
const { pipeline } = require("@xenova/transformers")
|
|
17
|
-
|
|
18
|
-
let embedderPipeline: any = null
|
|
19
|
-
|
|
20
|
-
async function getPipeline(): Promise<any> {
|
|
21
|
-
if (!embedderPipeline) {
|
|
22
|
-
embedderPipeline = await pipeline(
|
|
23
|
-
"feature-extraction",
|
|
24
|
-
"Xenova/all-MiniLM-L6-v2",
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
return embedderPipeline
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Embed text using all-MiniLM-L6-v2.
|
|
32
|
-
* Returns a 384-dimensional unit vector.
|
|
33
|
-
*/
|
|
34
|
-
export async function xenovaEmbed(text: string): Promise<number[]> {
|
|
35
|
-
const embedder = await getPipeline()
|
|
36
|
-
const output = await embedder(text, { pooling: "mean", normalize: true })
|
|
37
|
-
return Array.from(output.data) as number[]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Pre-warm the model so the first real query is not slow. */
|
|
41
|
-
export async function warmEmbedder(): Promise<void> {
|
|
42
|
-
await getPipeline()
|
|
43
|
-
}
|
package/modules/oracle/bm25.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight BM25 implementation for Pass 2 re-ranking.
|
|
3
|
-
*
|
|
4
|
-
* k1 = 1.5 (term frequency saturation)
|
|
5
|
-
* b = 0.75 (length normalization)
|
|
6
|
-
*
|
|
7
|
-
* Formula: score(q, d) = Σ IDF(qi) * f(qi, d) * (k1 + 1) / (f(qi, d) + k1 * (1 − b + b * |d| / avgdl))
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const K1 = 1.5
|
|
11
|
-
const B = 0.75
|
|
12
|
-
|
|
13
|
-
function tokenize(text: string): string[] {
|
|
14
|
-
return text.toLowerCase().match(/\b\w+\b/g) ?? []
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Robertson–Sparck Jones IDF with smoothing. */
|
|
18
|
-
function computeIdf(N: number, df: number): number {
|
|
19
|
-
return Math.log(1 + (N - df + 0.5) / (df + 0.5))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Score each document string against the query using BM25.
|
|
24
|
-
* Returns a score array parallel to `documents`.
|
|
25
|
-
*/
|
|
26
|
-
export function bm25Score(query: string, documents: string[]): number[] {
|
|
27
|
-
if (documents.length === 0) return []
|
|
28
|
-
|
|
29
|
-
const queryTokens = tokenize(query)
|
|
30
|
-
const docTokenLists = documents.map(tokenize)
|
|
31
|
-
const totalLength = docTokenLists.reduce((sum, d) => sum + d.length, 0)
|
|
32
|
-
const avgdl = totalLength / docTokenLists.length
|
|
33
|
-
const N = documents.length
|
|
34
|
-
|
|
35
|
-
// Precompute document frequency for each unique query token
|
|
36
|
-
const df = new Map<string, number>()
|
|
37
|
-
for (const token of queryTokens) {
|
|
38
|
-
if (!df.has(token)) {
|
|
39
|
-
df.set(token, docTokenLists.filter(doc => doc.includes(token)).length)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return docTokenLists.map(docTokenList => {
|
|
44
|
-
const docLength = docTokenList.length
|
|
45
|
-
|
|
46
|
-
const tf = new Map<string, number>()
|
|
47
|
-
for (const token of docTokenList) {
|
|
48
|
-
tf.set(token, (tf.get(token) ?? 0) + 1)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let score = 0
|
|
52
|
-
for (const token of queryTokens) {
|
|
53
|
-
const termFreq = tf.get(token) ?? 0
|
|
54
|
-
if (termFreq === 0) continue
|
|
55
|
-
const idfScore = computeIdf(N, df.get(token) ?? 0)
|
|
56
|
-
const normTf =
|
|
57
|
-
(termFreq * (K1 + 1)) /
|
|
58
|
-
(termFreq + K1 * (1 - B + B * (docLength / avgdl)))
|
|
59
|
-
score += idfScore * normTf
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return score
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const BM25_STOP_WORDS = new Set([
|
|
67
|
-
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
68
|
-
"have", "has", "had", "do", "does", "did", "will", "would", "should",
|
|
69
|
-
"could", "may", "might", "shall", "can", "to", "of", "in", "for", "on",
|
|
70
|
-
"with", "at", "by", "from", "as", "into", "through", "and", "or", "but",
|
|
71
|
-
"if", "then", "this", "that", "these", "those", "it", "its", "we", "they",
|
|
72
|
-
"their", "there", "when", "where", "what", "which", "who", "how", "not", "no",
|
|
73
|
-
])
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Extract domain terms from Chronicle key insights for Pass 2 query enrichment.
|
|
77
|
-
* Bridges the vocabulary gap between natural language queries and technical identifiers.
|
|
78
|
-
* Strips stop words, returns the most frequent distinctive tokens.
|
|
79
|
-
*/
|
|
80
|
-
export function extractDomainTerms(insights: string[]): string[] {
|
|
81
|
-
const allTokens = insights.flatMap(s => tokenize(s))
|
|
82
|
-
const freq = new Map<string, number>()
|
|
83
|
-
for (const token of allTokens) {
|
|
84
|
-
if (!BM25_STOP_WORDS.has(token) && token.length > 2) {
|
|
85
|
-
freq.set(token, (freq.get(token) ?? 0) + 1)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return [...freq.entries()]
|
|
89
|
-
.sort((a, b) => b[1] - a[1])
|
|
90
|
-
.slice(0, 10)
|
|
91
|
-
.map(([token]) => token)
|
|
92
|
-
}
|
package/modules/oracle/index.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export { query } from "./query"
|
|
2
|
-
export { propose, commit } from "./propose"
|
|
3
|
-
export type { OracleDeps, VectorStore } from "./types"
|
|
4
|
-
export type {
|
|
5
|
-
OracleResult,
|
|
6
|
-
QueryOptions,
|
|
7
|
-
ChronicleEntry,
|
|
8
|
-
OracleClient,
|
|
9
|
-
} from "../shared/types"
|
|
10
|
-
|
|
11
|
-
export { createLanceDBStore } from "./adapters/lance-db"
|
|
12
|
-
export { xenovaEmbed, warmEmbedder } from "./adapters/xenova-embedder"
|
|
13
|
-
|
|
14
|
-
import type { OracleClient } from "../shared/types"
|
|
15
|
-
import type { OracleDeps } from "./types"
|
|
16
|
-
import { query } from "./query"
|
|
17
|
-
import { propose, commit } from "./propose"
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create a bound OracleClient from injected deps.
|
|
21
|
-
* Pass this to Jury and Council — they only need the OracleClient interface,
|
|
22
|
-
* not the raw Oracle functions.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* const oracle = createOracleClient({
|
|
26
|
-
* embedder: xenovaEmbed,
|
|
27
|
-
* vectorStore: await createLanceDBStore(".chronicle"),
|
|
28
|
-
* })
|
|
29
|
-
*/
|
|
30
|
-
export function createOracleClient(deps: OracleDeps): OracleClient {
|
|
31
|
-
return {
|
|
32
|
-
query: (text, options) => query(text, options ?? {}, deps),
|
|
33
|
-
propose: entry => propose(entry, deps),
|
|
34
|
-
commit: proposalId => commit(proposalId, deps),
|
|
35
|
-
}
|
|
36
|
-
}
|
package/modules/oracle/log.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Append a query log entry to .chronicle/query-log.jsonl.
|
|
6
|
-
* Best-effort — callers should swallow errors from this.
|
|
7
|
-
*/
|
|
8
|
-
export async function appendQueryLog(
|
|
9
|
-
entry: Record<string, unknown>,
|
|
10
|
-
chronicleDir: string,
|
|
11
|
-
): Promise<void> {
|
|
12
|
-
await fs.mkdir(chronicleDir, { recursive: true })
|
|
13
|
-
const logPath = path.join(chronicleDir, "query-log.jsonl")
|
|
14
|
-
await fs.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8")
|
|
15
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import { randomUUID } from "crypto"
|
|
4
|
-
import { exec } from "child_process"
|
|
5
|
-
import { promisify } from "util"
|
|
6
|
-
import type { ChronicleEntry, SimilarityWarning } from "../shared/types"
|
|
7
|
-
import { entryText } from "../shared/types"
|
|
8
|
-
import type { OracleDeps } from "./types"
|
|
9
|
-
import { updateSummary } from "./summary"
|
|
10
|
-
|
|
11
|
-
const execAsync = promisify(exec)
|
|
12
|
-
|
|
13
|
-
const INSIGHT_MIN_LENGTH = 20
|
|
14
|
-
const INSIGHT_MAX_LENGTH = 200
|
|
15
|
-
const SIMILARITY_WARNING_THRESHOLD = 0.85
|
|
16
|
-
|
|
17
|
-
function validateEntry(entry: Omit<ChronicleEntry, "id" | "timestamp">): void {
|
|
18
|
-
const insight = entry.key_insight?.trim() ?? ""
|
|
19
|
-
if (insight.length < INSIGHT_MIN_LENGTH) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
`key_insight too short (${insight.length} chars, min ${INSIGHT_MIN_LENGTH}). ` +
|
|
22
|
-
`Write a specific, complete sentence naming the module or area affected.`,
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
if (insight.length > INSIGHT_MAX_LENGTH) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`key_insight too long (${insight.length} chars, max ${INSIGHT_MAX_LENGTH}). ` +
|
|
28
|
-
`Distil to a single clear sentence.`,
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
if (entry.decision !== undefined) {
|
|
32
|
-
const d = entry.decision.trim()
|
|
33
|
-
if (d.length < INSIGHT_MIN_LENGTH) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`decision too short (${d.length} chars, min ${INSIGHT_MIN_LENGTH}). ` +
|
|
36
|
-
`Write a specific, complete sentence describing the decision.`,
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
if (d.length > INSIGHT_MAX_LENGTH) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`decision too long (${d.length} chars, max ${INSIGHT_MAX_LENGTH}). ` +
|
|
42
|
-
`Distil to a single clear sentence.`,
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (!entry.affected_areas || entry.affected_areas.filter(a => a.trim()).length === 0) {
|
|
47
|
-
throw new Error(`affected_areas must contain at least one non-empty entry.`)
|
|
48
|
-
}
|
|
49
|
-
if (entry.confidence < 0 || entry.confidence > 1) {
|
|
50
|
-
throw new Error(`confidence must be between 0 and 1, got ${entry.confidence}.`)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function checkSimilarity(
|
|
55
|
-
entry: Omit<ChronicleEntry, "id" | "timestamp">,
|
|
56
|
-
deps: OracleDeps,
|
|
57
|
-
): Promise<SimilarityWarning | undefined> {
|
|
58
|
-
try {
|
|
59
|
-
const text = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
60
|
-
const vector = await deps.embedder(text)
|
|
61
|
-
const results = await deps.vectorStore.search(vector, 3)
|
|
62
|
-
if (results.length === 0) return undefined
|
|
63
|
-
const top = results[0]
|
|
64
|
-
if (top.score < SIMILARITY_WARNING_THRESHOLD) return undefined
|
|
65
|
-
return {
|
|
66
|
-
entry: top.entry,
|
|
67
|
-
score: top.score,
|
|
68
|
-
// If the existing entry is validated, a near-duplicate is likely a correction
|
|
69
|
-
warning: top.entry.status === "validated" ? "potential-supersession" : "potential-duplicate",
|
|
70
|
-
}
|
|
71
|
-
} catch {
|
|
72
|
-
// Similarity check is best-effort — never block a proposal because of it
|
|
73
|
-
return undefined
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Propose a new Chronicle entry for human review.
|
|
79
|
-
* Validates entry quality and checks for similar existing entries before writing.
|
|
80
|
-
* Writes the entry to .chronicle/proposals/<id>.json — NOT yet indexed.
|
|
81
|
-
* The proposal sits pending until a human calls commit() to approve it.
|
|
82
|
-
*
|
|
83
|
-
* Throws if key_insight is too short/long or affected_areas is empty.
|
|
84
|
-
* Returns a SimilarityWarning if a near-identical entry already exists —
|
|
85
|
-
* the human gate should surface this before approving the commit.
|
|
86
|
-
*/
|
|
87
|
-
export async function propose(
|
|
88
|
-
entry: Omit<ChronicleEntry, "id" | "timestamp">,
|
|
89
|
-
deps: OracleDeps,
|
|
90
|
-
): Promise<{ proposalId: string; similarity?: SimilarityWarning }> {
|
|
91
|
-
validateEntry(entry)
|
|
92
|
-
|
|
93
|
-
const similarity = await checkSimilarity(entry, deps)
|
|
94
|
-
|
|
95
|
-
const chronicleDir = deps.chronicleDir ?? ".chronicle"
|
|
96
|
-
const proposalsDir = path.join(chronicleDir, "proposals")
|
|
97
|
-
await fs.mkdir(proposalsDir, { recursive: true })
|
|
98
|
-
|
|
99
|
-
const proposalId = randomUUID()
|
|
100
|
-
const proposalPath = path.join(proposalsDir, `${proposalId}.json`)
|
|
101
|
-
await fs.writeFile(proposalPath, JSON.stringify(entry, null, 2), "utf8")
|
|
102
|
-
|
|
103
|
-
return { proposalId, ...(similarity ? { similarity } : {}) }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Commit a pending proposal after human approval.
|
|
108
|
-
* Reads the proposal file, assigns an ID and timestamp, embeds the entry,
|
|
109
|
-
* upserts it into the vector store, and deletes the proposal file.
|
|
110
|
-
*
|
|
111
|
-
* Throws if the proposal does not exist.
|
|
112
|
-
*/
|
|
113
|
-
export async function commit(
|
|
114
|
-
proposalId: string,
|
|
115
|
-
deps: OracleDeps,
|
|
116
|
-
): Promise<ChronicleEntry> {
|
|
117
|
-
const chronicleDir = deps.chronicleDir ?? ".chronicle"
|
|
118
|
-
const proposalPath = path.join(chronicleDir, "proposals", `${proposalId}.json`)
|
|
119
|
-
|
|
120
|
-
let raw: string
|
|
121
|
-
try {
|
|
122
|
-
raw = await fs.readFile(proposalPath, "utf8")
|
|
123
|
-
} catch {
|
|
124
|
-
throw new Error(`Proposal not found: ${proposalId}`)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const partial = JSON.parse(raw) as Omit<ChronicleEntry, "id" | "timestamp">
|
|
128
|
-
|
|
129
|
-
const entry: ChronicleEntry = {
|
|
130
|
-
...partial,
|
|
131
|
-
id: randomUUID(),
|
|
132
|
-
timestamp: new Date().toISOString(),
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Embed the primary text + areas + scope tags for richer retrieval
|
|
136
|
-
const embeddingText = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
137
|
-
const vector = await deps.embedder(embeddingText)
|
|
138
|
-
await deps.vectorStore.upsert(entry.id, vector, entry)
|
|
139
|
-
|
|
140
|
-
// Write to committed/ — the git-tracked source of truth shared across the team
|
|
141
|
-
const committedDir = path.join(chronicleDir, "committed")
|
|
142
|
-
await fs.mkdir(committedDir, { recursive: true })
|
|
143
|
-
const committedPath = path.join(committedDir, `${entry.id}.json`)
|
|
144
|
-
await fs.writeFile(committedPath, JSON.stringify(entry, null, 2), "utf8")
|
|
145
|
-
|
|
146
|
-
// Stage the committed entry for the next git commit — best-effort
|
|
147
|
-
try {
|
|
148
|
-
await execAsync(`git add "${committedPath}"`)
|
|
149
|
-
} catch {
|
|
150
|
-
// Not in a git repo, or git is unavailable — silently continue
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Rebuild SUMMARY.md — best-effort, never fail a commit
|
|
154
|
-
try {
|
|
155
|
-
await updateSummary(chronicleDir)
|
|
156
|
-
} catch {
|
|
157
|
-
// Summary generation failure must not fail a commit
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Remove the proposal — it has been committed
|
|
161
|
-
await fs.unlink(proposalPath)
|
|
162
|
-
|
|
163
|
-
return entry
|
|
164
|
-
}
|
package/modules/oracle/query.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import type { ChronicleEntry, OracleResult, QueryOptions } from "../shared/types"
|
|
2
|
-
import { entryText } from "../shared/types"
|
|
3
|
-
import type { OracleDeps } from "./types"
|
|
4
|
-
import { bm25Score, extractDomainTerms } from "./bm25"
|
|
5
|
-
import { appendQueryLog } from "./log"
|
|
6
|
-
|
|
7
|
-
const DEFAULT_LIMIT = 10
|
|
8
|
-
const DEFAULT_SCORE_THRESHOLD = 0.031
|
|
9
|
-
const RRF_K = 60
|
|
10
|
-
/** Retrieve this many vector candidates before BM25 re-ranking. */
|
|
11
|
-
const CANDIDATE_MULTIPLIER = 3
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Reciprocal Rank Fusion score.
|
|
15
|
-
* score = Σ 1 / (k + rank_i) summed across all rank lists.
|
|
16
|
-
* k = 60 (standard constant).
|
|
17
|
-
*/
|
|
18
|
-
function rrfScore(ranks: number[]): number {
|
|
19
|
-
return ranks.reduce((sum, rank) => sum + 1 / (RRF_K + rank), 0)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Two-pass retrieval with Reciprocal Rank Fusion.
|
|
24
|
-
*
|
|
25
|
-
* Pass 1 — vector similarity:
|
|
26
|
-
* Embed the query, retrieve top (limit × CANDIDATE_MULTIPLIER) candidates.
|
|
27
|
-
*
|
|
28
|
-
* Pass 2 — BM25 re-ranking with query enrichment:
|
|
29
|
-
* Extract domain terms from Pass 1 key insights, enrich the query,
|
|
30
|
-
* score candidates with BM25, fuse ranks via RRF.
|
|
31
|
-
*
|
|
32
|
-
* Results below scoreThreshold are dropped entirely.
|
|
33
|
-
* All queries are appended to .chronicle/query-log.jsonl.
|
|
34
|
-
*/
|
|
35
|
-
export async function query(
|
|
36
|
-
text: string,
|
|
37
|
-
options: QueryOptions = {},
|
|
38
|
-
deps: OracleDeps,
|
|
39
|
-
): Promise<OracleResult[]> {
|
|
40
|
-
const {
|
|
41
|
-
statusFilter,
|
|
42
|
-
limit = DEFAULT_LIMIT,
|
|
43
|
-
scoreThreshold = DEFAULT_SCORE_THRESHOLD,
|
|
44
|
-
} = options
|
|
45
|
-
|
|
46
|
-
const startTime = Date.now()
|
|
47
|
-
|
|
48
|
-
// ── Pass 1: vector similarity ──────────────────────────────────────────────
|
|
49
|
-
const queryVector = await deps.embedder(text)
|
|
50
|
-
const candidateLimit = limit * CANDIDATE_MULTIPLIER
|
|
51
|
-
let candidates = await deps.vectorStore.search(queryVector, candidateLimit)
|
|
52
|
-
|
|
53
|
-
// Status filter applied before BM25 to avoid scoring irrelevant entries
|
|
54
|
-
if (statusFilter && statusFilter.length > 0) {
|
|
55
|
-
candidates = candidates.filter(c => statusFilter.includes(c.entry.status))
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (candidates.length === 0) {
|
|
59
|
-
await tryLogQuery(text, [], startTime, deps)
|
|
60
|
-
return []
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ── Pass 2: BM25 re-ranking with query enrichment ─────────────────────────
|
|
64
|
-
const topInsights = candidates
|
|
65
|
-
.slice(0, Math.min(5, candidates.length))
|
|
66
|
-
.map(c => entryText(c.entry))
|
|
67
|
-
const domainTerms = extractDomainTerms(topInsights)
|
|
68
|
-
const enrichedQuery =
|
|
69
|
-
domainTerms.length > 0 ? `${text} ${domainTerms.join(" ")}` : text
|
|
70
|
-
|
|
71
|
-
const documents = candidates.map(c =>
|
|
72
|
-
[entryText(c.entry), ...c.entry.affected_areas, ...(c.entry.scope ?? [])].join(" "),
|
|
73
|
-
)
|
|
74
|
-
const bm25Scores = bm25Score(enrichedQuery, documents)
|
|
75
|
-
|
|
76
|
-
// Build BM25 rank lookup (index → rank)
|
|
77
|
-
const bm25RankOf: number[] = new Array(candidates.length)
|
|
78
|
-
bm25Scores
|
|
79
|
-
.map((score, i) => ({ i, score }))
|
|
80
|
-
.sort((a, b) => b.score - a.score)
|
|
81
|
-
.forEach(({ i }, rank) => {
|
|
82
|
-
bm25RankOf[i] = rank
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// ── RRF fusion ─────────────────────────────────────────────────────────────
|
|
86
|
-
const fused: Array<ChronicleEntry & { score: number }> = candidates.map(
|
|
87
|
-
(candidate, vectorRank) => ({
|
|
88
|
-
...candidate.entry,
|
|
89
|
-
score: rrfScore([vectorRank, bm25RankOf[vectorRank]]),
|
|
90
|
-
}),
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
fused.sort((a, b) => b.score - a.score)
|
|
94
|
-
|
|
95
|
-
const filtered = fused
|
|
96
|
-
.filter(r => r.score >= scoreThreshold)
|
|
97
|
-
.slice(0, limit)
|
|
98
|
-
|
|
99
|
-
const results = assignTiers(filtered)
|
|
100
|
-
|
|
101
|
-
await tryLogQuery(text, results, startTime, deps)
|
|
102
|
-
return results
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Assign relevance tiers within the result set using relative rank.
|
|
107
|
-
* Top ~30% → primary, next ~40% → supporting, remainder → background.
|
|
108
|
-
* Thresholds are relative so they self-calibrate as Chronicle grows.
|
|
109
|
-
*/
|
|
110
|
-
function assignTiers(
|
|
111
|
-
results: Array<ChronicleEntry & { score: number }>,
|
|
112
|
-
): OracleResult[] {
|
|
113
|
-
const n = results.length
|
|
114
|
-
if (n === 0) return []
|
|
115
|
-
const primaryCount = Math.max(1, Math.ceil(n * 0.3))
|
|
116
|
-
const supportingCount = Math.max(1, Math.ceil(n * 0.4))
|
|
117
|
-
return results.map((r, i) => ({
|
|
118
|
-
...r,
|
|
119
|
-
tier:
|
|
120
|
-
i < primaryCount ? "primary"
|
|
121
|
-
: i < primaryCount + supportingCount ? "supporting"
|
|
122
|
-
: "background",
|
|
123
|
-
}))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function tryLogQuery(
|
|
127
|
-
text: string,
|
|
128
|
-
results: OracleResult[],
|
|
129
|
-
startTime: number,
|
|
130
|
-
deps: OracleDeps,
|
|
131
|
-
): Promise<void> {
|
|
132
|
-
try {
|
|
133
|
-
await appendQueryLog(
|
|
134
|
-
{
|
|
135
|
-
query: text,
|
|
136
|
-
results: results.map(r => ({ id: r.id, score: r.score, status: r.status })),
|
|
137
|
-
resultCount: results.length,
|
|
138
|
-
durationMs: Date.now() - startTime,
|
|
139
|
-
timestamp: new Date().toISOString(),
|
|
140
|
-
},
|
|
141
|
-
deps.chronicleDir ?? ".chronicle",
|
|
142
|
-
)
|
|
143
|
-
} catch {
|
|
144
|
-
// Query logging is best-effort — never fail a query because of a log write failure
|
|
145
|
-
}
|
|
146
|
-
}
|