@balpal4495/quorum 0.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.
Files changed (39) hide show
  1. package/.github/copilot-instructions.md +94 -0
  2. package/CLAUDE.md +86 -0
  3. package/GEMINI.md +73 -0
  4. package/LICENSE +21 -0
  5. package/README.md +202 -0
  6. package/SETUP.md +256 -0
  7. package/bin/init.js +366 -0
  8. package/modules/AGENTS.md +66 -0
  9. package/modules/CLAUDE.md +64 -0
  10. package/modules/README.md +251 -0
  11. package/modules/council/advisors.ts +68 -0
  12. package/modules/council/chairman.ts +112 -0
  13. package/modules/council/deliberate.ts +106 -0
  14. package/modules/council/frame.ts +54 -0
  15. package/modules/council/index.ts +4 -0
  16. package/modules/council/personas.ts +57 -0
  17. package/modules/council/reviewers.ts +81 -0
  18. package/modules/council/types.ts +45 -0
  19. package/modules/jury/evaluate.ts +112 -0
  20. package/modules/jury/index.ts +3 -0
  21. package/modules/jury/schema.ts +15 -0
  22. package/modules/jury/types.ts +31 -0
  23. package/modules/oracle/adapters/lance-db.ts +81 -0
  24. package/modules/oracle/adapters/xenova-embedder.ts +43 -0
  25. package/modules/oracle/bm25.ts +92 -0
  26. package/modules/oracle/index.ts +36 -0
  27. package/modules/oracle/log.ts +15 -0
  28. package/modules/oracle/propose.ts +148 -0
  29. package/modules/oracle/query.ts +145 -0
  30. package/modules/oracle/summary.ts +115 -0
  31. package/modules/oracle/types.ts +32 -0
  32. package/modules/sentinel/assert.ts +95 -0
  33. package/modules/sentinel/coverage.ts +106 -0
  34. package/modules/sentinel/drift.ts +159 -0
  35. package/modules/sentinel/index.ts +6 -0
  36. package/modules/sentinel/review.ts +207 -0
  37. package/modules/setup.ts +153 -0
  38. package/modules/shared/types.ts +148 -0
  39. package/package.json +47 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Default advisor personas for the Council.
3
+ *
4
+ * Personas are interpretive lenses, not knowledge sources.
5
+ * All advisors receive the same Oracle evidence pack — their persona
6
+ * determines which entries they weight and how they read them.
7
+ *
8
+ * Add or replace personas in CouncilDeps to specialise for your domain.
9
+ */
10
+
11
+ export interface AdvisorPersona {
12
+ name: string
13
+ /** One-line description of this persona's evidence focus. */
14
+ lens: string
15
+ /** System prompt fragment injected into the advisor's prompt. */
16
+ systemFragment: string
17
+ }
18
+
19
+ export const DEFAULT_PERSONAS: readonly AdvisorPersona[] = [
20
+ {
21
+ name: "Pragmatist",
22
+ lens: "Weights validated entries — what has worked in this codebase",
23
+ systemFragment:
24
+ "Focus on `validated` Oracle entries. What has already worked in this codebase? " +
25
+ "Weight evidence that confirms the design will succeed based on prior outcomes.",
26
+ },
27
+ {
28
+ name: "Sceptic",
29
+ lens: "Weights refuted entries — what has failed and why",
30
+ systemFragment:
31
+ "Focus on `refuted` Oracle entries. What has already failed in this codebase and why? " +
32
+ "Look for signs this design repeats past mistakes. Surface failure modes explicitly.",
33
+ },
34
+ {
35
+ name: "Systems thinker",
36
+ lens: "Looks for patterns across all entries — second-order effects",
37
+ systemFragment:
38
+ "Read all Oracle entries as a system. Look for patterns, dependencies, and second-order " +
39
+ "effects. What does the design miss about how the system as a whole behaves?",
40
+ },
41
+ {
42
+ name: "Risk analyst",
43
+ lens: "Weights open entries — unresolved questions and unknowns",
44
+ systemFragment:
45
+ "Focus on `open` Oracle entries — unresolved questions and unknowns. " +
46
+ "What has not been confirmed? What uncertainty does this design carry? " +
47
+ "Flag every assumption that has not been validated by an outcome.",
48
+ },
49
+ {
50
+ name: "Evidence auditor",
51
+ lens: "Focuses on gaps — what Oracle does NOT contain",
52
+ systemFragment:
53
+ "Look for what is ABSENT from the Oracle evidence. What decisions is this design making " +
54
+ "without any codebase evidence to support them? " +
55
+ "Name every gap — a gap is not a reason to reject, but it must be surfaced.",
56
+ },
57
+ ]
@@ -0,0 +1,81 @@
1
+ import type { LLMProvider, OracleResult } from "../shared/types"
2
+ import type { AdvisorResponse } from "./advisors"
3
+
4
+ export interface ReviewerResponse {
5
+ reviewerId: string
6
+ review: string
7
+ }
8
+
9
+ /**
10
+ * Shuffle advisor responses and label them A–Z.
11
+ * Prevents reviewers deferring to confident responses by position or persona name.
12
+ */
13
+ function anonymise(responses: AdvisorResponse[]): string {
14
+ const shuffled = [...responses].sort(() => Math.random() - 0.5)
15
+ return shuffled
16
+ .map((r, i) => `## Advisor ${String.fromCharCode(65 + i)}\n${r.response}`)
17
+ .join("\n\n---\n\n")
18
+ }
19
+
20
+ function formatEvidenceSummary(evidence: OracleResult[]): string {
21
+ if (evidence.length === 0) return "No Oracle evidence available."
22
+ return evidence
23
+ .map(e => `[${e.id}] (${e.status}) ${e.key_insight}`)
24
+ .join("\n")
25
+ }
26
+
27
+ const REVIEWER_SYSTEM_PROMPT = [
28
+ "You are a Council reviewer. You evaluate the quality of advisor responses.",
29
+ "",
30
+ "You are NOT deciding whether the design is correct.",
31
+ "You are assessing the reasoning quality of each advisor response:",
32
+ "",
33
+ "1. Does the advisor actually use the Oracle evidence, or reason from general knowledge?",
34
+ "2. Are Oracle entry IDs cited? Do those citations match the evidence provided?",
35
+ "3. Is the response internally consistent?",
36
+ "4. Which responses provide the strongest evidence-backed reasoning?",
37
+ "5. Which responses make unsupported claims?",
38
+ "",
39
+ "Be critical. Evidence quality matters more than conclusion confidence.",
40
+ "Keep your review under 400 words.",
41
+ ].join("\n")
42
+
43
+ /**
44
+ * Run all reviewers in parallel.
45
+ * Each reviewer receives the anonymised advisor responses and the original evidence pack.
46
+ * Anonymisation prevents position bias and persona deference.
47
+ */
48
+ export async function fanOutReviewers(
49
+ advisorResponses: AdvisorResponse[],
50
+ evidence: OracleResult[],
51
+ reviewerCount: number,
52
+ llm: LLMProvider,
53
+ model?: string,
54
+ ): Promise<ReviewerResponse[]> {
55
+ const anonymisedResponses = anonymise(advisorResponses)
56
+ const evidenceSummary = formatEvidenceSummary(evidence)
57
+
58
+ return Promise.all(
59
+ Array.from({ length: reviewerCount }, async (_, i): Promise<ReviewerResponse> => {
60
+ const userPrompt = [
61
+ "## Advisor Responses (anonymised)",
62
+ anonymisedResponses,
63
+ "",
64
+ "## Oracle Evidence (for cross-referencing citations)",
65
+ evidenceSummary,
66
+ "",
67
+ "Review each advisor response for evidence quality.",
68
+ ].join("\n")
69
+
70
+ const review = await llm(
71
+ [
72
+ { role: "system", content: REVIEWER_SYSTEM_PROMPT },
73
+ { role: "user", content: userPrompt },
74
+ ],
75
+ model,
76
+ )
77
+
78
+ return { reviewerId: `reviewer-${i + 1}`, review }
79
+ }),
80
+ )
81
+ }
@@ -0,0 +1,45 @@
1
+ import type { OracleResult, LLMProvider, OracleClient } from "../shared/types"
2
+ import type { JuryOutput } from "../jury/types"
3
+
4
+ export interface CouncilInput {
5
+ /** What needs to be achieved. */
6
+ outcome: string
7
+ /** Proposed approach from the Designer. */
8
+ design: string
9
+ /** Same evidence pack the Jury received. */
10
+ evidence: OracleResult[]
11
+ /** Jury output — drives the council brief and confidence. */
12
+ jury_output: JuryOutput
13
+ }
14
+
15
+ export interface CouncilOutput {
16
+ satisfied: boolean
17
+ /** Chairman synthesis — every material conclusion cites Oracle entry IDs. */
18
+ verdict: string
19
+ /** What was challenged or could not be validated. */
20
+ challenges: string[]
21
+ /** Oracle entry IDs referenced in the verdict. */
22
+ evidence_cited: string[]
23
+ recommendation: "proceed" | "redesign" | "investigate-more"
24
+ }
25
+
26
+ export interface CouncilModels {
27
+ /** Model for the framer step. */
28
+ frame?: string
29
+ /** Model for advisors. High volume — cheaper model appropriate here. */
30
+ advisors?: string
31
+ /** Model for reviewers. Critical analysis — stronger model recommended. */
32
+ reviewers?: string
33
+ /** Model for the chairman. Synthesis — best available model recommended. */
34
+ chairman?: string
35
+ }
36
+
37
+ export interface CouncilDeps {
38
+ llm: LLMProvider
39
+ oracle: OracleClient
40
+ /** Number of advisors to run in parallel. Default: 5. */
41
+ advisorCount?: number
42
+ /** Number of reviewers to run in parallel. Default: 5. */
43
+ reviewerCount?: number
44
+ models?: CouncilModels
45
+ }
@@ -0,0 +1,112 @@
1
+ import type { JuryInput, JuryOutput, JuryDeps } from "./types"
2
+ import type { OracleResult } from "../shared/types"
3
+ import { JuryOutputSchema } from "./schema"
4
+
5
+ const CONFIDENCE_THRESHOLD = 0.6
6
+
7
+ function formatEvidence(evidence: OracleResult[]): string {
8
+ if (evidence.length === 0) {
9
+ return "No Oracle entries found. There is no prior evidence for this codebase on this topic."
10
+ }
11
+ return evidence
12
+ .map(e =>
13
+ [
14
+ `[${e.id}] status=${e.status} confidence=${e.confidence.toFixed(2)} score=${e.score.toFixed(3)}`,
15
+ `Insight: ${e.key_insight}`,
16
+ `Areas: ${e.affected_areas.join(", ")}`,
17
+ e.outcome ? `Outcome: ${e.outcome}` : null,
18
+ ]
19
+ .filter(Boolean)
20
+ .join("\n"),
21
+ )
22
+ .join("\n\n")
23
+ }
24
+
25
+ const SYSTEM_PROMPT = `You are the Jury — an evidence-based evaluator for agentic development workflows.
26
+
27
+ Your job is to evaluate a proposed design against Oracle evidence and produce a structured confidence score.
28
+ You do NOT make decisions. You assess and score. Your output determines the Council's brief.
29
+
30
+ Score the design across these four dimensions (equally weighted to produce a final confidence in [0, 1]):
31
+ 1. Evidence support — do validated Oracle entries confirm this approach works in this codebase?
32
+ 2. Feasibility — do Oracle entries (or their absence) suggest this is achievable?
33
+ 3. Risk — what do refuted entries reveal about failure modes? Has this been tried and failed?
34
+ 4. Completeness — does the design address the full outcome, or only part of it?
35
+
36
+ council_brief is determined by confidence only (do not invent a value):
37
+ confidence < 0.6 → council_brief = "challenge"
38
+ confidence ≥ 0.6 → council_brief = "pressure-test"
39
+
40
+ Return ONLY valid JSON that matches this schema exactly — no markdown fences, no explanation:
41
+ {
42
+ "confidence": <number 0–1>,
43
+ "assessment": <string — what the evidence supports or contradicts>,
44
+ "gaps": [<string — each missing piece of evidence from Oracle>],
45
+ "council_brief": "challenge" | "pressure-test",
46
+ "recommendation": "proceed" | "investigate-more" | "redesign"
47
+ }`
48
+
49
+ /**
50
+ * Evaluate a proposed design against Oracle evidence.
51
+ *
52
+ * Scores across four dimensions (evidence support, feasibility, risk, completeness)
53
+ * and returns a structured JuryOutput. The council_brief is always derived from the
54
+ * confidence score — the LLM value is overridden to ensure deterministic routing.
55
+ *
56
+ * Throws if the LLM returns non-JSON or a response that fails schema validation.
57
+ * Never silently defaults to a passing score.
58
+ */
59
+ export async function evaluate(
60
+ input: JuryInput,
61
+ deps: JuryDeps,
62
+ ): Promise<JuryOutput> {
63
+ const { llm, model } = deps
64
+ const evidenceText = formatEvidence(input.evidence)
65
+
66
+ const userPrompt = [
67
+ "## Outcome",
68
+ input.outcome,
69
+ "",
70
+ "## Proposed Design",
71
+ input.design,
72
+ "",
73
+ "## Oracle Evidence",
74
+ evidenceText,
75
+ ].join("\n")
76
+
77
+ const raw = await llm(
78
+ [
79
+ { role: "system", content: SYSTEM_PROMPT },
80
+ { role: "user", content: userPrompt },
81
+ ],
82
+ model,
83
+ )
84
+
85
+ let parsed: unknown
86
+ try {
87
+ const cleaned = raw
88
+ .replace(/^```(?:json)?\s*/m, "")
89
+ .replace(/\s*```$/m, "")
90
+ .trim()
91
+ parsed = JSON.parse(cleaned)
92
+ } catch {
93
+ throw new Error(
94
+ `Jury: LLM returned non-JSON response. Raw (first 300 chars): ${raw.slice(0, 300)}`,
95
+ )
96
+ }
97
+
98
+ const result = JuryOutputSchema.safeParse(parsed)
99
+ if (!result.success) {
100
+ throw new Error(
101
+ `Jury: LLM output failed schema validation. Issues: ${JSON.stringify(result.error.issues)}`,
102
+ )
103
+ }
104
+
105
+ const output = result.data
106
+
107
+ // Enforce council_brief from confidence — do not trust the LLM to compute this correctly
108
+ output.council_brief =
109
+ output.confidence < CONFIDENCE_THRESHOLD ? "challenge" : "pressure-test"
110
+
111
+ return output
112
+ }
@@ -0,0 +1,3 @@
1
+ export { evaluate } from "./evaluate"
2
+ export type { JuryInput, JuryOutput, JuryDeps } from "./types"
3
+ export { JuryOutputSchema } from "./schema"
@@ -0,0 +1,15 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * Zod schema for the Jury's structured LLM output.
5
+ * evaluate() validates all LLM responses against this before returning.
6
+ */
7
+ export const JuryOutputSchema = z.object({
8
+ confidence: z.number().min(0).max(1),
9
+ assessment: z.string().min(1),
10
+ gaps: z.array(z.string()),
11
+ council_brief: z.enum(["challenge", "pressure-test"]),
12
+ recommendation: z.enum(["proceed", "investigate-more", "redesign"]),
13
+ })
14
+
15
+ export type JuryOutputParsed = z.infer<typeof JuryOutputSchema>
@@ -0,0 +1,31 @@
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
+ export interface JuryOutput {
13
+ /** 0–1 confidence score. Drives the Council brief. */
14
+ confidence: number
15
+ /** What the evidence supports or contradicts. */
16
+ assessment: string
17
+ /** Evidence missing from Oracle that would improve confidence. */
18
+ gaps: string[]
19
+ /**
20
+ * Council brief derived from confidence:
21
+ * < 0.6 → "challenge" (find what is wrong — broader scope)
22
+ * ≥ 0.6 → "pressure-test" (assume correct, try to break it)
23
+ */
24
+ council_brief: "challenge" | "pressure-test"
25
+ recommendation: "proceed" | "investigate-more" | "redesign"
26
+ }
27
+
28
+ export interface JuryDeps {
29
+ llm: LLMProvider
30
+ model?: string
31
+ }
@@ -0,0 +1,81 @@
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
+ }
@@ -0,0 +1,43 @@
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
+ }
@@ -0,0 +1,92 @@
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
+ }
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,15 @@
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
+ }