@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.
@@ -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})\n${e.key_insight}\nAreas: ${e.affected_areas.join(", ")}`,
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.key_insight}`,
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.key_insight}`)
24
+ .map(e => `[${e.id}] (${e.status}) ${entryText(e)}`)
24
25
  .join("\n")
25
26
  }
26
27
 
@@ -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.key_insight}`,
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.key_insight, ...entry.affected_areas].join(" ")
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 key insight (plus affected areas for richer retrieval)
120
- const embeddingText = [entry.key_insight, ...entry.affected_areas].join(" ")
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
 
@@ -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.key_insight)
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.key_insight, ...c.entry.affected_areas].join(" "),
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.key_insight}`
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:\n"${entry.key_insight}"\n\n` +
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.key_insight,
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.key_insight,
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.key_insight}`)
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.key_insight, ...entry.affected_areas].join(" ")
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
  }
@@ -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
- /** The core finding or decision, in one clear sentence. */
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
- /** Parts of the codebase or system this entry applies to. */
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balpal4495/quorum",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
4
4
  "description": "Portable reasoning layer for agentic codebases — Oracle, Jury, Council, Sentinel",
5
5
  "type": "module",
6
6
  "license": "MIT",