@balpal4495/quorum 0.1.10 → 0.2.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/modules/council/advisors.ts +4 -1
- package/modules/council/chairman.ts +2 -1
- package/modules/council/deliberate.ts +5 -0
- package/modules/council/reviewers.ts +2 -1
- package/modules/jury/evaluate.ts +3 -2
- package/modules/oracle/propose.ts +19 -3
- package/modules/oracle/query.ts +3 -2
- package/modules/oracle/summary.ts +2 -1
- package/modules/sentinel/drift.ts +7 -3
- package/modules/sentinel/review.ts +2 -1
- package/modules/setup.ts +2 -1
- package/modules/shared/types.ts +39 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LLMProvider, OracleResult } from "../shared/types"
|
|
2
|
+
import { entryText } from "../shared/types"
|
|
2
3
|
import type { AdvisorPersona } from "./personas"
|
|
3
4
|
|
|
4
5
|
export interface AdvisorResponse {
|
|
@@ -12,7 +13,9 @@ function formatEvidence(evidence: OracleResult[]): string {
|
|
|
12
13
|
}
|
|
13
14
|
return evidence
|
|
14
15
|
.map(e =>
|
|
15
|
-
`[${e.id}] (${e.status})
|
|
16
|
+
`[${e.id}] (${e.status})
|
|
17
|
+
${entryText(e)}
|
|
18
|
+
Areas: ${e.affected_areas.join(", ")}${e.scope ? " | " + e.scope.join(", ") : ""}`,
|
|
16
19
|
)
|
|
17
20
|
.join("\n\n")
|
|
18
21
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
2
|
import type { LLMProvider, OracleResult } from "../shared/types"
|
|
3
|
+
import { entryText } from "../shared/types"
|
|
3
4
|
import type { AdvisorResponse } from "./advisors"
|
|
4
5
|
import type { ReviewerResponse } from "./reviewers"
|
|
5
6
|
import type { CouncilOutput } from "./types"
|
|
@@ -29,7 +30,7 @@ function formatEvidence(evidence: OracleResult[]): string {
|
|
|
29
30
|
return evidence
|
|
30
31
|
.map(
|
|
31
32
|
e =>
|
|
32
|
-
`[${e.id}] (${e.status}, confidence: ${e.confidence.toFixed(2)}) ${e
|
|
33
|
+
`[${e.id}] (${e.status}, confidence: ${e.confidence.toFixed(2)}) ${entryText(e)}`,
|
|
33
34
|
)
|
|
34
35
|
.join("\n")
|
|
35
36
|
}
|
|
@@ -82,8 +82,13 @@ export async function deliberate(
|
|
|
82
82
|
.slice(0, 200)
|
|
83
83
|
|
|
84
84
|
await oracle.propose({
|
|
85
|
+
schema_version: 2,
|
|
86
|
+
topic: input.outcome.slice(0, 80),
|
|
87
|
+
decision: keyInsight,
|
|
85
88
|
key_insight: keyInsight,
|
|
86
89
|
affected_areas: extractAffectedAreas(input.outcome, input.design),
|
|
90
|
+
alternatives_considered: verdict.challenges,
|
|
91
|
+
rejected_reason: verdict.satisfied ? [] : [verdict.verdict.slice(0, 200)],
|
|
87
92
|
status: "open",
|
|
88
93
|
confidence: input.jury_output.confidence,
|
|
89
94
|
source_module: "council",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LLMProvider, OracleResult } from "../shared/types"
|
|
2
|
+
import { entryText } from "../shared/types"
|
|
2
3
|
import type { AdvisorResponse } from "./advisors"
|
|
3
4
|
|
|
4
5
|
export interface ReviewerResponse {
|
|
@@ -20,7 +21,7 @@ function anonymise(responses: AdvisorResponse[]): string {
|
|
|
20
21
|
function formatEvidenceSummary(evidence: OracleResult[]): string {
|
|
21
22
|
if (evidence.length === 0) return "No Oracle evidence available."
|
|
22
23
|
return evidence
|
|
23
|
-
.map(e => `[${e.id}] (${e.status}) ${e
|
|
24
|
+
.map(e => `[${e.id}] (${e.status}) ${entryText(e)}`)
|
|
24
25
|
.join("\n")
|
|
25
26
|
}
|
|
26
27
|
|
package/modules/jury/evaluate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JuryInput, JuryOutput, JuryDeps } from "./types"
|
|
2
2
|
import type { OracleResult } from "../shared/types"
|
|
3
|
+
import { entryText } from "../shared/types"
|
|
3
4
|
import { JuryOutputSchema } from "./schema"
|
|
4
5
|
|
|
5
6
|
const CONFIDENCE_THRESHOLD = 0.6
|
|
@@ -12,8 +13,8 @@ function formatEvidence(evidence: OracleResult[]): string {
|
|
|
12
13
|
.map(e =>
|
|
13
14
|
[
|
|
14
15
|
`[${e.id}] status=${e.status} confidence=${e.confidence.toFixed(2)} score=${e.score.toFixed(3)}`,
|
|
15
|
-
`Insight: ${e
|
|
16
|
-
`Areas: ${e.affected_areas.join(", ")}`,
|
|
16
|
+
`Insight: ${entryText(e)}`,
|
|
17
|
+
`Areas: ${e.affected_areas.join(", ")}${e.scope ? " | " + e.scope.join(", ") : ""}`,
|
|
17
18
|
e.outcome ? `Outcome: ${e.outcome}` : null,
|
|
18
19
|
]
|
|
19
20
|
.filter(Boolean)
|
|
@@ -4,6 +4,7 @@ import { randomUUID } from "crypto"
|
|
|
4
4
|
import { exec } from "child_process"
|
|
5
5
|
import { promisify } from "util"
|
|
6
6
|
import type { ChronicleEntry, SimilarityWarning } from "../shared/types"
|
|
7
|
+
import { entryText } from "../shared/types"
|
|
7
8
|
import type { OracleDeps } from "./types"
|
|
8
9
|
import { updateSummary } from "./summary"
|
|
9
10
|
|
|
@@ -27,6 +28,21 @@ function validateEntry(entry: Omit<ChronicleEntry, "id" | "timestamp">): void {
|
|
|
27
28
|
`Distil to a single clear sentence.`,
|
|
28
29
|
)
|
|
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
|
+
}
|
|
30
46
|
if (!entry.affected_areas || entry.affected_areas.filter(a => a.trim()).length === 0) {
|
|
31
47
|
throw new Error(`affected_areas must contain at least one non-empty entry.`)
|
|
32
48
|
}
|
|
@@ -40,7 +56,7 @@ async function checkSimilarity(
|
|
|
40
56
|
deps: OracleDeps,
|
|
41
57
|
): Promise<SimilarityWarning | undefined> {
|
|
42
58
|
try {
|
|
43
|
-
const text = [entry.
|
|
59
|
+
const text = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
44
60
|
const vector = await deps.embedder(text)
|
|
45
61
|
const results = await deps.vectorStore.search(vector, 3)
|
|
46
62
|
if (results.length === 0) return undefined
|
|
@@ -116,8 +132,8 @@ export async function commit(
|
|
|
116
132
|
timestamp: new Date().toISOString(),
|
|
117
133
|
}
|
|
118
134
|
|
|
119
|
-
// Embed the
|
|
120
|
-
const embeddingText = [entry.
|
|
135
|
+
// Embed the primary text + areas + scope tags for richer retrieval
|
|
136
|
+
const embeddingText = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
121
137
|
const vector = await deps.embedder(embeddingText)
|
|
122
138
|
await deps.vectorStore.upsert(entry.id, vector, entry)
|
|
123
139
|
|
package/modules/oracle/query.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChronicleEntry, OracleResult, QueryOptions } from "../shared/types"
|
|
2
|
+
import { entryText } from "../shared/types"
|
|
2
3
|
import type { OracleDeps } from "./types"
|
|
3
4
|
import { bm25Score, extractDomainTerms } from "./bm25"
|
|
4
5
|
import { appendQueryLog } from "./log"
|
|
@@ -62,13 +63,13 @@ export async function query(
|
|
|
62
63
|
// ── Pass 2: BM25 re-ranking with query enrichment ─────────────────────────
|
|
63
64
|
const topInsights = candidates
|
|
64
65
|
.slice(0, Math.min(5, candidates.length))
|
|
65
|
-
.map(c => c.entry
|
|
66
|
+
.map(c => entryText(c.entry))
|
|
66
67
|
const domainTerms = extractDomainTerms(topInsights)
|
|
67
68
|
const enrichedQuery =
|
|
68
69
|
domainTerms.length > 0 ? `${text} ${domainTerms.join(" ")}` : text
|
|
69
70
|
|
|
70
71
|
const documents = candidates.map(c =>
|
|
71
|
-
[c.entry.
|
|
72
|
+
[entryText(c.entry), ...c.entry.affected_areas, ...(c.entry.scope ?? [])].join(" "),
|
|
72
73
|
)
|
|
73
74
|
const bm25Scores = bm25Score(enrichedQuery, documents)
|
|
74
75
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import type { ChronicleEntry } from "../shared/types"
|
|
4
|
+
import { entryText } from "../shared/types"
|
|
4
5
|
|
|
5
6
|
const SUMMARY_WEEKS = 12
|
|
6
7
|
const DIRECTIVE =
|
|
@@ -29,7 +30,7 @@ function workRefLabel(entry: ChronicleEntry): string {
|
|
|
29
30
|
function renderEntry(entry: ChronicleEntry): string {
|
|
30
31
|
const areas = entry.affected_areas.join(", ")
|
|
31
32
|
const id = entry.id.slice(0, 8)
|
|
32
|
-
return `- **[${id}]** ${areas} — \`${entry.status}\` (${entry.confidence.toFixed(2)}) — ${entry
|
|
33
|
+
return `- **[${id}]** ${areas} — \`${entry.status}\` (${entry.confidence.toFixed(2)}) — ${entryText(entry)}`
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import type { ChronicleEntry, DriftFlag, DriftReport, LLMProvider } from "../shared/types"
|
|
4
|
+
import { entryText } from "../shared/types"
|
|
4
5
|
|
|
5
6
|
const FILE_CONTENT_LIMIT = 3000
|
|
6
7
|
|
|
@@ -73,7 +74,10 @@ async function evaluateDrift(
|
|
|
73
74
|
{
|
|
74
75
|
role: "user",
|
|
75
76
|
content:
|
|
76
|
-
`Documented insight
|
|
77
|
+
`Documented insight:
|
|
78
|
+
"${entryText(entry)}"
|
|
79
|
+
|
|
80
|
+
` +
|
|
77
81
|
`Current source:\n${fileSection}\n\n` +
|
|
78
82
|
`Does this insight still accurately describe the code above?\n` +
|
|
79
83
|
`{"stillValid": boolean, "confidence": number, "reasoning": "one sentence"}`,
|
|
@@ -86,7 +90,7 @@ async function evaluateDrift(
|
|
|
86
90
|
const parsed = JSON.parse(match[0]) as { stillValid?: unknown; confidence?: unknown; reasoning?: unknown }
|
|
87
91
|
return {
|
|
88
92
|
entryId: entry.id,
|
|
89
|
-
keyInsight: entry
|
|
93
|
+
keyInsight: entryText(entry),
|
|
90
94
|
affectedFiles: files.map(f => f.filePath),
|
|
91
95
|
stillValid: Boolean(parsed.stillValid),
|
|
92
96
|
confidence: typeof parsed.confidence === "number" ? Math.max(0, Math.min(1, parsed.confidence)) : 0.5,
|
|
@@ -96,7 +100,7 @@ async function evaluateDrift(
|
|
|
96
100
|
// Parse failure → conservative: flag for human review
|
|
97
101
|
return {
|
|
98
102
|
entryId: entry.id,
|
|
99
|
-
keyInsight: entry
|
|
103
|
+
keyInsight: entryText(entry),
|
|
100
104
|
affectedFiles: files.map(f => f.filePath),
|
|
101
105
|
stillValid: false,
|
|
102
106
|
confidence: 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import type { ChronicleEntry } from "../shared/types"
|
|
4
|
+
import { entryText } from "../shared/types"
|
|
4
5
|
import { coverage as runCoverage } from "./coverage"
|
|
5
6
|
|
|
6
7
|
function extractModule(filePath: string): string {
|
|
@@ -192,7 +193,7 @@ export async function reviewContext(
|
|
|
192
193
|
lines.push(`**${stat.name}/**`)
|
|
193
194
|
const relevant = allEntries.filter(e => stat.entryIds.includes(e.id))
|
|
194
195
|
for (const entry of relevant) {
|
|
195
|
-
lines.push(`- \`[${entry.id.slice(0, 8)}]\` ${entry
|
|
196
|
+
lines.push(`- \`[${entry.id.slice(0, 8)}]\` ${entryText(entry)}`)
|
|
196
197
|
lines.push(` *${entry.status} — confidence ${entry.confidence.toFixed(2)}*`)
|
|
197
198
|
}
|
|
198
199
|
lines.push("")
|
package/modules/setup.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createLanceDBStore } from "./oracle/adapters/lance-db"
|
|
|
6
6
|
import { evaluate } from "./jury/evaluate"
|
|
7
7
|
import { deliberate } from "./council/deliberate"
|
|
8
8
|
import type { LLMProvider, OracleClient } from "./shared/types"
|
|
9
|
+
import { entryText } from "./shared/types"
|
|
9
10
|
import type { JuryInput, JuryOutput, JuryDeps } from "./jury/types"
|
|
10
11
|
import type { CouncilInput, CouncilOutput, CouncilDeps, CouncilModels } from "./council/types"
|
|
11
12
|
|
|
@@ -124,7 +125,7 @@ export async function setup(options: SetupOptions): Promise<Modules> {
|
|
|
124
125
|
for (const file of missing) {
|
|
125
126
|
const raw = await fs.readFile(path.join(committedDir, file), "utf8")
|
|
126
127
|
const entry = JSON.parse(raw) as import("./shared/types").ChronicleEntry
|
|
127
|
-
const embeddingText = [entry.
|
|
128
|
+
const embeddingText = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
128
129
|
const vector = await embedder(embeddingText)
|
|
129
130
|
await vectorStore.upsert(entry.id, vector, entry)
|
|
130
131
|
}
|
package/modules/shared/types.ts
CHANGED
|
@@ -29,12 +29,19 @@ export type WorkRef = {
|
|
|
29
29
|
/**
|
|
30
30
|
* A durable knowledge record stored in Chronicle.
|
|
31
31
|
* This is the canonical unit of institutional memory.
|
|
32
|
+
*
|
|
33
|
+
* Schema versions:
|
|
34
|
+
* v1 (no schema_version field): key_insight is the primary text field.
|
|
35
|
+
* v2 (schema_version: 2): decision is the primary text field; key_insight is a copy
|
|
36
|
+
* of decision written for backwards compatibility. Always use entryText() to read.
|
|
32
37
|
*/
|
|
33
38
|
export type ChronicleEntry = {
|
|
34
39
|
id: string
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
// ── v1 fields (required — always present) ───────────────────────────────
|
|
42
|
+
/** The core finding or decision, in one clear sentence. v2: copy of decision. */
|
|
36
43
|
key_insight: string
|
|
37
|
-
/**
|
|
44
|
+
/** File paths or system areas this entry applies to. Used by Sentinel for file matching. */
|
|
38
45
|
affected_areas: string[]
|
|
39
46
|
status: "validated" | "refuted" | "open"
|
|
40
47
|
/** 0–1. How strongly this was confirmed at write time. */
|
|
@@ -48,6 +55,36 @@ export type ChronicleEntry = {
|
|
|
48
55
|
/** The unit of work that triggered this entry. Used to build SUMMARY.md temporal context. */
|
|
49
56
|
work_ref?: WorkRef
|
|
50
57
|
timestamp: string
|
|
58
|
+
|
|
59
|
+
// ── v2 fields (optional — absent on legacy entries) ──────────────────────
|
|
60
|
+
/** 2 = decision record format. Absent = v1 legacy entry. */
|
|
61
|
+
schema_version?: 2
|
|
62
|
+
/** Short label for this decision. e.g. "auth/session strategy" */
|
|
63
|
+
topic?: string
|
|
64
|
+
/** The decision itself — the primary text field in v2. Use entryText() to read. */
|
|
65
|
+
decision?: string
|
|
66
|
+
/** Domain/category tags. Additive — does NOT replace affected_areas. e.g. ["auth", "sessions"] */
|
|
67
|
+
scope?: string[]
|
|
68
|
+
/** Approaches that were considered but not chosen. */
|
|
69
|
+
alternatives_considered?: string[]
|
|
70
|
+
/** Why the alternatives were rejected. */
|
|
71
|
+
rejected_reason?: string[]
|
|
72
|
+
/** ID of the Chronicle entry this supersedes. */
|
|
73
|
+
supersedes?: string | null
|
|
74
|
+
/** ID of the Chronicle entry that superseded this one. */
|
|
75
|
+
superseded_by?: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return the primary text for a Chronicle entry regardless of schema version.
|
|
80
|
+
* v2 entries use decision; v1 entries use key_insight.
|
|
81
|
+
* All callsites that render or embed entry text must use this function.
|
|
82
|
+
*
|
|
83
|
+
* Accepts any object with key_insight and optional decision — works for both
|
|
84
|
+
* full ChronicleEntry and Omit<ChronicleEntry, "id" | "timestamp"> from propose().
|
|
85
|
+
*/
|
|
86
|
+
export function entryText(entry: { key_insight: string; decision?: string }): string {
|
|
87
|
+
return entry.decision ?? entry.key_insight
|
|
51
88
|
}
|
|
52
89
|
|
|
53
90
|
/**
|