@balpal4495/quorum 3.4.0 → 3.5.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/README.md +37 -0
- package/bin/__tests__/mcp-server.test.js +5 -4
- package/bin/__tests__/mcp-tools.test.js +13 -13
- package/bin/commands/serve.js +23 -4
- package/bin/mcp/server.js +36 -2
- package/bin/mcp/tools.js +80 -15
- package/bin/ui/app.html +416 -3
- package/dist/oracle/adapters/lance-db.d.ts.map +1 -1
- package/dist/oracle/adapters/lance-db.js +2 -1
- package/dist/oracle/adapters/lance-db.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -380,6 +380,43 @@ Writes to `.chronicle/committed/`, updates `SUMMARY.md`, removes the proposal. A
|
|
|
380
380
|
|
|
381
381
|
---
|
|
382
382
|
|
|
383
|
+
### `quorum serve` — governance UI + MCP server
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
quorum serve # starts on http://localhost:4242
|
|
387
|
+
quorum serve --port 8080 # custom port
|
|
388
|
+
quorum serve --no-llm # disable LLM auto-detection
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Starts a single HTTP server that provides:
|
|
392
|
+
|
|
393
|
+
- **Governance UI** at `/` — browse committed entries, review and approve pending proposals (with inline evidence, Jury breakdown, and Council conditions), track Chronicle health, and run Compass product-direction analysis.
|
|
394
|
+
- **MCP server** at `POST /mcp` — JSON-RPC 2.0 endpoint exposing 10 tools and 6 resources to any MCP-compatible agent.
|
|
395
|
+
- **REST API** — used by the UI and accessible directly:
|
|
396
|
+
|
|
397
|
+
| Route | Description |
|
|
398
|
+
|---|---|
|
|
399
|
+
| `GET /api/entries?q=<query>` | Search committed Chronicle entries |
|
|
400
|
+
| `GET /api/proposals` | List pending proposals |
|
|
401
|
+
| `GET /api/coverage` | Chronicle coverage map |
|
|
402
|
+
| `GET /api/growth` | Chronicle health score |
|
|
403
|
+
| `GET /api/compass?subcommand=map\|brief\|opportunities\|bets` | Compass product direction |
|
|
404
|
+
| `PATCH /api/proposals/:id` | Edit a pending proposal before committing |
|
|
405
|
+
|
|
406
|
+
**MCP resources** (readable by agents via `resources/read`):
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
chronicle://summary chronicle://proposals
|
|
410
|
+
chronicle://coverage chronicle://growth
|
|
411
|
+
chronicle://compass chronicle://entry/{id}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**MCP tools**: `quorum_query`, `quorum_brief`, `quorum_stage`, `quorum_pending`, `quorum_coverage`, `quorum_growth`, `quorum_help`, `quorum_advisor`\*, `quorum_check`, `quorum_compass`\*
|
|
415
|
+
|
|
416
|
+
\* LLM-powered tools auto-activate when `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY` is set. Without a key they return a `no-llm` status with CLI fallback instructions.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
383
420
|
### `quorum growth` — is Chronicle actually learning?
|
|
384
421
|
|
|
385
422
|
```bash
|
|
@@ -474,7 +474,7 @@ describe("POST /mcp — tools/call", () => {
|
|
|
474
474
|
expect(typeof result.content).toBe("string")
|
|
475
475
|
})
|
|
476
476
|
|
|
477
|
-
it("quorum_advisor returns
|
|
477
|
+
it("quorum_advisor returns no-llm status when no provider is configured", async () => {
|
|
478
478
|
const { dir } = await makeChronicle(tmpDir)
|
|
479
479
|
srv = await startServer(tmpDir, dir)
|
|
480
480
|
|
|
@@ -485,7 +485,7 @@ describe("POST /mcp — tools/call", () => {
|
|
|
485
485
|
|
|
486
486
|
expect(body.error).toBeUndefined()
|
|
487
487
|
const result = JSON.parse(body.result.content[0].text)
|
|
488
|
-
expect(result.status).toBe("
|
|
488
|
+
expect(result.status).toBe("no-llm")
|
|
489
489
|
})
|
|
490
490
|
|
|
491
491
|
it("returns -32601 for unknown tool name", async () => {
|
|
@@ -555,7 +555,7 @@ describe("unknown route", () => {
|
|
|
555
555
|
// ── MCP Resources ─────────────────────────────────────────────────────────────
|
|
556
556
|
|
|
557
557
|
describe("POST /mcp — resources/list", () => {
|
|
558
|
-
it("returns all
|
|
558
|
+
it("returns all six chronicle:// resources", async () => {
|
|
559
559
|
const { dir } = await makeChronicle(tmpDir)
|
|
560
560
|
srv = await startServer(tmpDir, dir)
|
|
561
561
|
|
|
@@ -570,7 +570,8 @@ describe("POST /mcp — resources/list", () => {
|
|
|
570
570
|
expect(uris).toContain("chronicle://coverage")
|
|
571
571
|
expect(uris).toContain("chronicle://growth")
|
|
572
572
|
expect(uris).toContain("chronicle://entry/{id}")
|
|
573
|
-
expect(uris).
|
|
573
|
+
expect(uris).toContain("chronicle://compass")
|
|
574
|
+
expect(uris).toHaveLength(6)
|
|
574
575
|
})
|
|
575
576
|
|
|
576
577
|
it("each resource has a name, description, and mimeType", async () => {
|
|
@@ -489,37 +489,37 @@ describe("toolHelp", () => {
|
|
|
489
489
|
})
|
|
490
490
|
})
|
|
491
491
|
|
|
492
|
-
// ──
|
|
492
|
+
// ── LLM-powered tools (no-llm fallback when no provider configured) ───────────
|
|
493
493
|
|
|
494
|
-
describe("toolAdvisor
|
|
494
|
+
describe("toolAdvisor", () => {
|
|
495
495
|
it("throws when question is missing", async () => {
|
|
496
496
|
await expect(toolAdvisor({})).rejects.toThrow("question is required")
|
|
497
497
|
})
|
|
498
498
|
|
|
499
|
-
it("returns status=
|
|
499
|
+
it("returns status=no-llm with a message when no provider is set", async () => {
|
|
500
500
|
const result = await toolAdvisor({ question: "what was decided about retries?" })
|
|
501
|
-
expect(result.status).toBe("
|
|
502
|
-
expect(result.message).toMatch(/
|
|
501
|
+
expect(result.status).toBe("no-llm")
|
|
502
|
+
expect(result.message).toMatch(/quorum advisor/i)
|
|
503
503
|
})
|
|
504
504
|
})
|
|
505
505
|
|
|
506
|
-
describe("toolCheck
|
|
506
|
+
describe("toolCheck", () => {
|
|
507
507
|
it("throws when neither outcome nor design is provided", async () => {
|
|
508
508
|
await expect(toolCheck({})).rejects.toThrow("outcome or design is required")
|
|
509
509
|
})
|
|
510
510
|
|
|
511
|
-
it("returns
|
|
511
|
+
it("returns preflight and risk when called with outcome and design", async () => {
|
|
512
512
|
const result = await toolCheck({ outcome: "ship safely", design: "use feature flags" })
|
|
513
|
-
expect(result
|
|
514
|
-
expect(result
|
|
513
|
+
expect(result).toHaveProperty("preflight")
|
|
514
|
+
expect(result).toHaveProperty("risk")
|
|
515
515
|
})
|
|
516
516
|
})
|
|
517
517
|
|
|
518
|
-
describe("toolCompass
|
|
519
|
-
it("returns status=
|
|
518
|
+
describe("toolCompass", () => {
|
|
519
|
+
it("returns status=no-llm with a message when no provider is set", async () => {
|
|
520
520
|
const result = await toolCompass({ subcommand: "brief" })
|
|
521
|
-
expect(result.status).toBe("
|
|
522
|
-
expect(result.message).toMatch(/
|
|
521
|
+
expect(result.status).toBe("no-llm")
|
|
522
|
+
expect(result.message).toMatch(/quorum compass/i)
|
|
523
523
|
})
|
|
524
524
|
})
|
|
525
525
|
|
package/bin/commands/serve.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { c } from "../shared/colors.js"
|
|
2
2
|
import { findChronicleDir } from "../shared/chronicle.js"
|
|
3
3
|
import { createServer } from "../mcp/server.js"
|
|
4
|
+
import { detectProvider } from "../shared/llm.js"
|
|
4
5
|
|
|
5
6
|
function parseArgs(argv) {
|
|
6
7
|
const portIdx = argv.indexOf("--port")
|
|
@@ -9,11 +10,12 @@ function parseArgs(argv) {
|
|
|
9
10
|
: Number(argv.find(a => /^\d{2,5}$/.test(a)) ?? 3000)
|
|
10
11
|
const hostIdx = argv.indexOf("--host")
|
|
11
12
|
const host = hostIdx !== -1 ? argv[hostIdx + 1] : "localhost"
|
|
12
|
-
|
|
13
|
+
const noLlm = argv.includes("--no-llm")
|
|
14
|
+
return { port: isNaN(port) ? 3000 : port, host, noLlm }
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export async function run(argv) {
|
|
16
|
-
const { port, host } = parseArgs(argv)
|
|
18
|
+
const { port, host, noLlm } = parseArgs(argv)
|
|
17
19
|
|
|
18
20
|
const projectRoot = process.cwd()
|
|
19
21
|
const chronicleDir = await findChronicleDir(projectRoot)
|
|
@@ -23,14 +25,31 @@ export async function run(argv) {
|
|
|
23
25
|
process.exit(1)
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
// Auto-detect LLM provider unless --no-llm is passed
|
|
29
|
+
let llmProvider = null
|
|
30
|
+
let llmName = null
|
|
31
|
+
if (!noLlm) {
|
|
32
|
+
const detected = await detectProvider()
|
|
33
|
+
if (detected) {
|
|
34
|
+
llmProvider = detected.llm
|
|
35
|
+
llmName = detected.name
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const server = await createServer({ projectRoot, chronicleDir, llm: llmProvider })
|
|
27
40
|
|
|
28
41
|
server.listen(port, host, () => {
|
|
29
42
|
const base = `http://${host}:${port}`
|
|
30
43
|
console.log(`\n${c.bold("Quorum")} ${c.dim(`serving ${projectRoot}`)}\n`)
|
|
31
44
|
console.log(` ${c.cyan("UI")} ${c.dim(base + "/")}`)
|
|
32
45
|
console.log(` ${c.cyan("MCP")} ${c.dim(base + "/mcp")}`)
|
|
33
|
-
console.log(` ${c.cyan("Chronicle")} ${c.dim(chronicleDir)}
|
|
46
|
+
console.log(` ${c.cyan("Chronicle")} ${c.dim(chronicleDir)}`)
|
|
47
|
+
if (llmName) {
|
|
48
|
+
console.log(` ${c.cyan("LLM")} ${c.dim(llmName + " (advisor/check/compass enabled)")}`)
|
|
49
|
+
} else {
|
|
50
|
+
console.log(` ${c.cyan("LLM")} ${c.dim("none — LLM tools return CLI fallback hints")}`)
|
|
51
|
+
}
|
|
52
|
+
console.log()
|
|
34
53
|
console.log(c.bold("Claude Desktop") + c.dim(" — add to claude_desktop_config.json:"))
|
|
35
54
|
console.log(c.dim(JSON.stringify({
|
|
36
55
|
mcpServers: { quorum: { type: "streamable-http", url: `${base}/mcp` } }
|
package/bin/mcp/server.js
CHANGED
|
@@ -27,8 +27,11 @@ import {
|
|
|
27
27
|
toolBrief,
|
|
28
28
|
toolCoverage,
|
|
29
29
|
toolGrowth,
|
|
30
|
+
toolCompass,
|
|
30
31
|
commitProposal,
|
|
31
32
|
deleteProposal,
|
|
33
|
+
updateProposal,
|
|
34
|
+
setLLM,
|
|
32
35
|
} from "./tools.js"
|
|
33
36
|
|
|
34
37
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -124,10 +127,16 @@ async function handleMCP(body, defaultProjectRoot) {
|
|
|
124
127
|
description: "Chronicle health score, entry counts, and guidance.",
|
|
125
128
|
mimeType: "application/json",
|
|
126
129
|
},
|
|
130
|
+
{
|
|
131
|
+
uri: "chronicle://compass",
|
|
132
|
+
name: "Compass product direction",
|
|
133
|
+
description: "Latest Compass map: behaviours, gaps, and opportunities detected from the codebase.",
|
|
134
|
+
mimeType: "application/json",
|
|
135
|
+
},
|
|
127
136
|
{
|
|
128
137
|
uriTemplate: "chronicle://entry/{id}",
|
|
129
138
|
name: "Chronicle entry",
|
|
130
|
-
description: "A single committed Chronicle entry by
|
|
139
|
+
description: "A single committed Chronicle entry by id or 8-char prefix.",
|
|
131
140
|
mimeType: "application/json",
|
|
132
141
|
},
|
|
133
142
|
],
|
|
@@ -160,6 +169,11 @@ async function handleMCP(body, defaultProjectRoot) {
|
|
|
160
169
|
return rpcOk(id, { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }] })
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
if (uri === "chronicle://compass") {
|
|
173
|
+
const result = await toolCompass({ subcommand: "map", projectRoot: defaultProjectRoot })
|
|
174
|
+
return rpcOk(id, { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }] })
|
|
175
|
+
}
|
|
176
|
+
|
|
163
177
|
// chronicle://entry/{id}
|
|
164
178
|
const entryMatch = uri.match(/^chronicle:\/\/entry\/(.+)$/)
|
|
165
179
|
if (entryMatch) {
|
|
@@ -198,7 +212,9 @@ function setCORS(res) {
|
|
|
198
212
|
|
|
199
213
|
// ── Server factory ────────────────────────────────────────────────────────────
|
|
200
214
|
|
|
201
|
-
export async function createServer({ projectRoot, chronicleDir }) {
|
|
215
|
+
export async function createServer({ projectRoot, chronicleDir, llm = null }) {
|
|
216
|
+
// Wire LLM into tools module so advisor/check/compass MCP tools work
|
|
217
|
+
setLLM(llm)
|
|
202
218
|
let uiHtml
|
|
203
219
|
try {
|
|
204
220
|
uiHtml = await fs.readFile(UI_PATH, "utf8")
|
|
@@ -284,6 +300,24 @@ export async function createServer({ projectRoot, chronicleDir }) {
|
|
|
284
300
|
}
|
|
285
301
|
}
|
|
286
302
|
|
|
303
|
+
// ── REST: edit/patch proposal ───────────────────────────────────────────
|
|
304
|
+
if (proposalMatch && req.method === "PATCH") {
|
|
305
|
+
try {
|
|
306
|
+
const body = await readBody(req)
|
|
307
|
+
const result = await updateProposal(proposalMatch[1], body, chronicleDir)
|
|
308
|
+
return json(res, 200, result)
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return json(res, 400, { error: err.message })
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── REST: compass ───────────────────────────────────────────────────────
|
|
315
|
+
if (pathname === "/api/compass" && req.method === "GET") {
|
|
316
|
+
const subcommand = new URL(req.url, "http://localhost").searchParams.get("subcommand") ?? "map"
|
|
317
|
+
const result = await toolCompass({ subcommand, projectRoot })
|
|
318
|
+
return json(res, 200, result)
|
|
319
|
+
}
|
|
320
|
+
|
|
287
321
|
// ── Web UI ──────────────────────────────────────────────────────────────
|
|
288
322
|
if ((pathname === "/" || pathname === "/index.html") && req.method === "GET") {
|
|
289
323
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" })
|
package/bin/mcp/tools.js
CHANGED
|
@@ -263,26 +263,70 @@ export async function toolHelp({ topic } = {}) {
|
|
|
263
263
|
return { topic, content: section }
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
// ──
|
|
267
|
-
// These
|
|
268
|
-
//
|
|
269
|
-
// "not yet available" message rather than silently failing.
|
|
266
|
+
// ── LLM-powered tools ─────────────────────────────────────────────────────────
|
|
267
|
+
// These are activated when quorum serve detects an LLM provider.
|
|
268
|
+
// Without a provider they return a clear CLI-fallback hint.
|
|
270
269
|
|
|
271
|
-
|
|
272
|
-
`${name} requires an LLM provider. This is planned — run 'quorum ${name.replace("quorum_", "")}' from the CLI for now, or watch for a future 'quorum serve --llm' flag.`
|
|
270
|
+
let _llm = null // set by createTools() at server startup
|
|
273
271
|
|
|
274
|
-
|
|
272
|
+
const NO_LLM = (name) => ({
|
|
273
|
+
status: "no-llm",
|
|
274
|
+
message: `${name} requires an LLM provider. No provider was detected at startup. ` +
|
|
275
|
+
`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY and restart quorum serve. ` +
|
|
276
|
+
`Alternatively run 'quorum ${name.replace("quorum_", "")}' from the CLI.`,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
export async function toolAdvisor({ question, projectRoot } = {}) {
|
|
275
280
|
if (!question) throw new Error("question is required")
|
|
276
|
-
|
|
281
|
+
if (!_llm) return NO_LLM("quorum_advisor")
|
|
282
|
+
|
|
283
|
+
const { ask } = await import("../../dist/advisor/index.js")
|
|
284
|
+
const { chronicleDir } = await resolve(projectRoot)
|
|
285
|
+
const { createOracleClient } = await import("../../dist/oracle/index.js")
|
|
286
|
+
const { xenovaEmbed } = await import("../../dist/oracle/adapters/xenova-embedder.js")
|
|
287
|
+
const { createLanceDBStore } = await import("../../dist/oracle/adapters/lance-db.js")
|
|
288
|
+
|
|
289
|
+
const store = await createLanceDBStore(chronicleDir)
|
|
290
|
+
const oracle = createOracleClient({ store, embed: xenovaEmbed })
|
|
291
|
+
const result = await ask({ question, oracle, llm: _llm })
|
|
292
|
+
return result
|
|
277
293
|
}
|
|
278
294
|
|
|
279
|
-
export async function toolCheck({ outcome, design } = {}) {
|
|
295
|
+
export async function toolCheck({ outcome, design, projectRoot } = {}) {
|
|
296
|
+
// quorum check is LLM-free — uses the same preflight + risk classifier as the CLI
|
|
280
297
|
if (!outcome && !design) throw new Error("outcome or design is required")
|
|
281
|
-
|
|
298
|
+
const { runPreflight, classifyRisk } = await import("../shared/patterns.js")
|
|
299
|
+
const preflight = runPreflight(outcome ?? "", design ?? "")
|
|
300
|
+
const risk = classifyRisk(outcome ?? "", design ?? "")
|
|
301
|
+
return { preflight, risk }
|
|
282
302
|
}
|
|
283
303
|
|
|
284
|
-
export async function toolCompass({ subcommand } = {}) {
|
|
285
|
-
|
|
304
|
+
export async function toolCompass({ subcommand = "brief", goal, idea, projectRoot } = {}) {
|
|
305
|
+
if (!_llm) return NO_LLM("quorum_compass")
|
|
306
|
+
|
|
307
|
+
const { chronicleDir } = await resolve(projectRoot)
|
|
308
|
+
// Delegate to the compass CLI command handler for now
|
|
309
|
+
const { run: compassRun } = await import("../commands/compass.js")
|
|
310
|
+
// Capture stdout
|
|
311
|
+
const captured = []
|
|
312
|
+
const origWrite = process.stdout.write.bind(process.stdout)
|
|
313
|
+
process.stdout.write = (chunk, ...rest) => { captured.push(String(chunk)); return true }
|
|
314
|
+
try {
|
|
315
|
+
const extraArgs = []
|
|
316
|
+
if (subcommand === "pathways" && goal) extraArgs.push("--goal", goal)
|
|
317
|
+
if (subcommand === "score" && idea) extraArgs.push("--idea", idea)
|
|
318
|
+
await compassRun([subcommand, ...extraArgs])
|
|
319
|
+
} finally {
|
|
320
|
+
process.stdout.write = origWrite
|
|
321
|
+
}
|
|
322
|
+
return { subcommand, output: captured.join("").trim() }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Call once at server startup to wire the LLM provider into LLM-powered tools.
|
|
327
|
+
*/
|
|
328
|
+
export function setLLM(llmProvider) {
|
|
329
|
+
_llm = llmProvider
|
|
286
330
|
}
|
|
287
331
|
|
|
288
332
|
// ── Proposal commit (human-gate — UI only, never an MCP AI tool) ──────────────
|
|
@@ -316,6 +360,27 @@ export async function deleteProposal(proposalId, chronicleDir) {
|
|
|
316
360
|
return { deleted: match.replace(".json", "") }
|
|
317
361
|
}
|
|
318
362
|
|
|
363
|
+
export async function updateProposal(proposalId, patch, chronicleDir) {
|
|
364
|
+
const ALLOWED = ["topic", "decision", "key_insight", "status", "confidence",
|
|
365
|
+
"affected_areas", "scope", "alternatives_considered", "rejected_reason"]
|
|
366
|
+
const proposalsDir = path.join(chronicleDir, "proposals")
|
|
367
|
+
const files = await fs.readdir(proposalsDir).catch(() => [])
|
|
368
|
+
const match = files.find(f => f === `${proposalId}.json` || f.startsWith(proposalId))
|
|
369
|
+
if (!match) throw new Error(`Proposal not found: ${proposalId}`)
|
|
370
|
+
|
|
371
|
+
const proposalPath = path.join(proposalsDir, match)
|
|
372
|
+
const raw = await fs.readFile(proposalPath, "utf8")
|
|
373
|
+
const current = JSON.parse(raw)
|
|
374
|
+
|
|
375
|
+
// Only apply allowed fields — never let PATCH overwrite id/proposalId/schema_version
|
|
376
|
+
for (const key of ALLOWED) {
|
|
377
|
+
if (patch[key] !== undefined) current[key] = patch[key]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await fs.writeFile(proposalPath, JSON.stringify(current, null, 2), "utf8")
|
|
381
|
+
return { updated: match.replace(".json", ""), topic: current.topic }
|
|
382
|
+
}
|
|
383
|
+
|
|
319
384
|
// ── MCP tool registry ─────────────────────────────────────────────────────────
|
|
320
385
|
|
|
321
386
|
export const MCP_TOOLS = [
|
|
@@ -416,7 +481,7 @@ export const MCP_TOOLS = [
|
|
|
416
481
|
// ── [TODO] LLM-powered tools ──
|
|
417
482
|
{
|
|
418
483
|
name: "quorum_advisor",
|
|
419
|
-
description: "
|
|
484
|
+
description: "Ask a plain-language question answered from Chronicle using an LLM. Returns a synthesised answer with evidence citations. Auto-activated when quorum serve detects an API key.",
|
|
420
485
|
inputSchema: {
|
|
421
486
|
type: "object",
|
|
422
487
|
properties: {
|
|
@@ -428,7 +493,7 @@ export const MCP_TOOLS = [
|
|
|
428
493
|
},
|
|
429
494
|
{
|
|
430
495
|
name: "quorum_check",
|
|
431
|
-
description: "
|
|
496
|
+
description: "Run instant risk triage on a design against Chronicle patterns — no LLM required. Returns preflight flags and a risk level (low/medium/high/critical).",
|
|
432
497
|
inputSchema: {
|
|
433
498
|
type: "object",
|
|
434
499
|
properties: {
|
|
@@ -440,7 +505,7 @@ export const MCP_TOOLS = [
|
|
|
440
505
|
},
|
|
441
506
|
{
|
|
442
507
|
name: "quorum_compass",
|
|
443
|
-
description: "
|
|
508
|
+
description: "Product-direction synthesis — behaviours, pathways, bets, idea scoring. Auto-activated when quorum serve detects an LLM provider. Use subcommand: brief | map | pathways | bets | score | opportunities.",
|
|
444
509
|
inputSchema: {
|
|
445
510
|
type: "object",
|
|
446
511
|
properties: {
|
package/bin/ui/app.html
CHANGED
|
@@ -351,6 +351,179 @@
|
|
|
351
351
|
::-webkit-scrollbar { width: 6px; }
|
|
352
352
|
::-webkit-scrollbar-track { background: transparent; }
|
|
353
353
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
354
|
+
|
|
355
|
+
/* ── Growth ── */
|
|
356
|
+
.health-grid {
|
|
357
|
+
display: grid;
|
|
358
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
359
|
+
gap: 12px;
|
|
360
|
+
margin-bottom: 24px;
|
|
361
|
+
}
|
|
362
|
+
.health-stat {
|
|
363
|
+
background: var(--surface);
|
|
364
|
+
border: 1px solid var(--border);
|
|
365
|
+
border-radius: var(--radius);
|
|
366
|
+
padding: 14px 16px;
|
|
367
|
+
}
|
|
368
|
+
.health-stat-val {
|
|
369
|
+
font-size: 28px;
|
|
370
|
+
font-weight: 700;
|
|
371
|
+
color: var(--text);
|
|
372
|
+
line-height: 1;
|
|
373
|
+
}
|
|
374
|
+
.health-stat-lbl {
|
|
375
|
+
font-size: 11px;
|
|
376
|
+
color: var(--muted);
|
|
377
|
+
margin-top: 4px;
|
|
378
|
+
text-transform: uppercase;
|
|
379
|
+
letter-spacing: .06em;
|
|
380
|
+
}
|
|
381
|
+
.health-score-wrap {
|
|
382
|
+
display: flex;
|
|
383
|
+
align-items: center;
|
|
384
|
+
gap: 16px;
|
|
385
|
+
margin-bottom: 24px;
|
|
386
|
+
background: var(--surface);
|
|
387
|
+
border: 1px solid var(--border);
|
|
388
|
+
border-radius: var(--radius);
|
|
389
|
+
padding: 20px 24px;
|
|
390
|
+
}
|
|
391
|
+
.health-score-num {
|
|
392
|
+
font-size: 52px;
|
|
393
|
+
font-weight: 800;
|
|
394
|
+
line-height: 1;
|
|
395
|
+
}
|
|
396
|
+
.health-score-text { flex: 1; }
|
|
397
|
+
.health-score-label {
|
|
398
|
+
font-size: 13px;
|
|
399
|
+
font-weight: 700;
|
|
400
|
+
text-transform: uppercase;
|
|
401
|
+
letter-spacing: .08em;
|
|
402
|
+
margin-bottom: 4px;
|
|
403
|
+
}
|
|
404
|
+
.health-hint { font-size: 13px; color: var(--muted); }
|
|
405
|
+
.hint-thriving { color: var(--green); }
|
|
406
|
+
.hint-healthy { color: var(--accent); }
|
|
407
|
+
.hint-slow { color: var(--yellow); }
|
|
408
|
+
.hint-stalled { color: var(--red); }
|
|
409
|
+
|
|
410
|
+
/* ── Compass ── */
|
|
411
|
+
.compass-output {
|
|
412
|
+
background: var(--surface);
|
|
413
|
+
border: 1px solid var(--border);
|
|
414
|
+
border-radius: var(--radius);
|
|
415
|
+
padding: 16px;
|
|
416
|
+
font-family: var(--mono);
|
|
417
|
+
font-size: 12px;
|
|
418
|
+
white-space: pre-wrap;
|
|
419
|
+
color: var(--muted);
|
|
420
|
+
max-height: 500px;
|
|
421
|
+
overflow-y: auto;
|
|
422
|
+
line-height: 1.6;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* ── Edit modal ── */
|
|
426
|
+
.modal-overlay {
|
|
427
|
+
display: none;
|
|
428
|
+
position: fixed;
|
|
429
|
+
inset: 0;
|
|
430
|
+
background: rgba(0,0,0,.65);
|
|
431
|
+
z-index: 200;
|
|
432
|
+
align-items: center;
|
|
433
|
+
justify-content: center;
|
|
434
|
+
}
|
|
435
|
+
.modal-overlay.open { display: flex; }
|
|
436
|
+
.modal {
|
|
437
|
+
background: var(--surface);
|
|
438
|
+
border: 1px solid var(--border);
|
|
439
|
+
border-radius: 10px;
|
|
440
|
+
padding: 24px;
|
|
441
|
+
width: min(560px, 94vw);
|
|
442
|
+
max-height: 90vh;
|
|
443
|
+
overflow-y: auto;
|
|
444
|
+
}
|
|
445
|
+
.modal h3 { font-size: 15px; font-weight: 700; margin-bottom: 16px; }
|
|
446
|
+
.field { margin-bottom: 14px; }
|
|
447
|
+
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 5px; }
|
|
448
|
+
.field input, .field textarea, .field select {
|
|
449
|
+
width: 100%;
|
|
450
|
+
padding: 8px 12px;
|
|
451
|
+
background: var(--bg);
|
|
452
|
+
border: 1px solid var(--border);
|
|
453
|
+
border-radius: var(--radius);
|
|
454
|
+
color: var(--text);
|
|
455
|
+
font: inherit;
|
|
456
|
+
font-size: 13px;
|
|
457
|
+
outline: none;
|
|
458
|
+
transition: border-color .15s;
|
|
459
|
+
resize: vertical;
|
|
460
|
+
}
|
|
461
|
+
.field input:focus, .field textarea:focus, .field select:focus { border-color: var(--accent); }
|
|
462
|
+
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
|
|
463
|
+
|
|
464
|
+
/* ── Evidence/jury breakdown on proposals ── */
|
|
465
|
+
.evidence-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
|
466
|
+
.evidence-tag {
|
|
467
|
+
font-family: var(--mono);
|
|
468
|
+
font-size: 10px;
|
|
469
|
+
padding: 2px 6px;
|
|
470
|
+
border-radius: 4px;
|
|
471
|
+
background: rgba(124,110,255,.12);
|
|
472
|
+
color: var(--accent);
|
|
473
|
+
border: 1px solid rgba(124,110,255,.2);
|
|
474
|
+
}
|
|
475
|
+
.breakdown-row {
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
gap: 8px;
|
|
479
|
+
font-size: 11px;
|
|
480
|
+
color: var(--muted);
|
|
481
|
+
margin-top: 3px;
|
|
482
|
+
}
|
|
483
|
+
.breakdown-label { width: 128px; flex-shrink: 0; }
|
|
484
|
+
.breakdown-bar {
|
|
485
|
+
flex: 1;
|
|
486
|
+
height: 3px;
|
|
487
|
+
border-radius: 2px;
|
|
488
|
+
background: var(--border);
|
|
489
|
+
overflow: hidden;
|
|
490
|
+
}
|
|
491
|
+
.breakdown-fill { height: 100%; border-radius: 2px; background: var(--accent); }
|
|
492
|
+
.breakdown-pct { width: 28px; text-align: right; color: var(--muted); }
|
|
493
|
+
.jury-section {
|
|
494
|
+
margin-top: 10px;
|
|
495
|
+
padding: 8px 10px;
|
|
496
|
+
background: rgba(124,110,255,.05);
|
|
497
|
+
border: 1px solid rgba(124,110,255,.12);
|
|
498
|
+
border-radius: 6px;
|
|
499
|
+
}
|
|
500
|
+
.jury-section-title {
|
|
501
|
+
font-size: 10px;
|
|
502
|
+
font-weight: 700;
|
|
503
|
+
text-transform: uppercase;
|
|
504
|
+
letter-spacing: .07em;
|
|
505
|
+
color: var(--muted);
|
|
506
|
+
margin-bottom: 6px;
|
|
507
|
+
}
|
|
508
|
+
.council-section {
|
|
509
|
+
margin-top: 8px;
|
|
510
|
+
padding: 8px 10px;
|
|
511
|
+
background: rgba(82,168,224,.05);
|
|
512
|
+
border: 1px solid rgba(82,168,224,.12);
|
|
513
|
+
border-radius: 6px;
|
|
514
|
+
font-size: 12px;
|
|
515
|
+
color: var(--blue);
|
|
516
|
+
}
|
|
517
|
+
.council-section-title {
|
|
518
|
+
font-size: 10px;
|
|
519
|
+
font-weight: 700;
|
|
520
|
+
text-transform: uppercase;
|
|
521
|
+
letter-spacing: .07em;
|
|
522
|
+
color: var(--muted);
|
|
523
|
+
margin-bottom: 6px;
|
|
524
|
+
}
|
|
525
|
+
.council-section ul { margin: 0 0 0 14px; }
|
|
526
|
+
.council-section li { margin-bottom: 2px; color: var(--blue); }
|
|
354
527
|
</style>
|
|
355
528
|
</head>
|
|
356
529
|
<body>
|
|
@@ -361,6 +534,8 @@
|
|
|
361
534
|
<button class="active" onclick="showTab('chronicle')">Chronicle</button>
|
|
362
535
|
<button onclick="showTab('proposals')">Proposals <span class="badge" id="proposalCount" style="display:none"></span></button>
|
|
363
536
|
<button onclick="showTab('coverage')">Coverage</button>
|
|
537
|
+
<button onclick="showTab('growth')">Growth</button>
|
|
538
|
+
<button onclick="showTab('compass')">Compass</button>
|
|
364
539
|
</nav>
|
|
365
540
|
</header>
|
|
366
541
|
|
|
@@ -386,8 +561,63 @@
|
|
|
386
561
|
<p class="section-sub">Source files with Chronicle entries referencing them.</p>
|
|
387
562
|
<div id="coverageView"><div class="loading">Loading…</div></div>
|
|
388
563
|
</div>
|
|
564
|
+
|
|
565
|
+
<!-- ── Growth tab ───────────────────────────────────────────────── -->
|
|
566
|
+
<div id="tab-growth" class="tab">
|
|
567
|
+
<h2 class="section-heading">Growth</h2>
|
|
568
|
+
<p class="section-sub">Chronicle memory health — how actively this codebase is learning.</p>
|
|
569
|
+
<div id="growthView"><div class="loading">Loading…</div></div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<!-- ── Compass tab ──────────────────────────────────────────────── -->
|
|
573
|
+
<div id="tab-compass" class="tab">
|
|
574
|
+
<h2 class="section-heading">Compass</h2>
|
|
575
|
+
<p class="section-sub">Product-direction synthesis — behaviours, gaps, and opportunities.</p>
|
|
576
|
+
<div class="toolbar" style="margin-bottom:20px">
|
|
577
|
+
<select id="compassSubcmd" style="max-width:200px;flex:none;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font:inherit;font-size:13px;outline:none">
|
|
578
|
+
<option value="map">Behaviour map</option>
|
|
579
|
+
<option value="brief">Direction brief</option>
|
|
580
|
+
<option value="opportunities">Opportunities</option>
|
|
581
|
+
<option value="bets">Strategic bets</option>
|
|
582
|
+
</select>
|
|
583
|
+
<button class="btn" onclick="loadCompass()" style="flex:none">Run</button>
|
|
584
|
+
</div>
|
|
585
|
+
<div id="compassView"><div class="empty">Select a subcommand and click Run.<small>Requires an LLM provider configured for quorum serve.</small></div></div>
|
|
586
|
+
</div>
|
|
389
587
|
</main>
|
|
390
588
|
|
|
589
|
+
<!-- ── Edit proposal modal ───────────────────────────────────────────── -->
|
|
590
|
+
<div class="modal-overlay" id="editModal" onclick="closeEditModal(event)">
|
|
591
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
592
|
+
<h3>Edit proposal</h3>
|
|
593
|
+
<input type="hidden" id="editId">
|
|
594
|
+
<div class="field">
|
|
595
|
+
<label>Topic</label>
|
|
596
|
+
<input type="text" id="editTopic">
|
|
597
|
+
</div>
|
|
598
|
+
<div class="field">
|
|
599
|
+
<label>Decision / key insight</label>
|
|
600
|
+
<textarea id="editDecision" rows="5"></textarea>
|
|
601
|
+
</div>
|
|
602
|
+
<div class="field">
|
|
603
|
+
<label>Status</label>
|
|
604
|
+
<select id="editStatus">
|
|
605
|
+
<option value="open">open</option>
|
|
606
|
+
<option value="validated">validated</option>
|
|
607
|
+
<option value="refuted">refuted</option>
|
|
608
|
+
</select>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="field">
|
|
611
|
+
<label>Confidence (0–1)</label>
|
|
612
|
+
<input type="number" id="editConfidence" min="0" max="1" step="0.05">
|
|
613
|
+
</div>
|
|
614
|
+
<div class="modal-actions">
|
|
615
|
+
<button class="btn" onclick="closeEditModal()">Cancel</button>
|
|
616
|
+
<button class="btn btn-approve" onclick="saveEdit()">Save changes</button>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
391
621
|
<div id="toast"></div>
|
|
392
622
|
|
|
393
623
|
<script>
|
|
@@ -396,6 +626,7 @@
|
|
|
396
626
|
let allEntries = []
|
|
397
627
|
let allProposals = []
|
|
398
628
|
let coverageData = null
|
|
629
|
+
let growthData = null
|
|
399
630
|
let searchTimer = null
|
|
400
631
|
let activeTab = "chronicle"
|
|
401
632
|
|
|
@@ -404,19 +635,22 @@ let activeTab = "chronicle"
|
|
|
404
635
|
window.addEventListener("DOMContentLoaded", () => {
|
|
405
636
|
loadChronicle()
|
|
406
637
|
loadProposals()
|
|
407
|
-
//
|
|
638
|
+
// Other tabs loaded lazily on first open
|
|
408
639
|
})
|
|
409
640
|
|
|
410
641
|
// ── Tab switching ──────────────────────────────────────────────────────────
|
|
411
642
|
|
|
643
|
+
const TAB_NAMES = ["chronicle", "proposals", "coverage", "growth", "compass"]
|
|
644
|
+
|
|
412
645
|
function showTab(name) {
|
|
413
646
|
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"))
|
|
414
647
|
document.querySelectorAll("nav button").forEach((b, i) => {
|
|
415
|
-
b.classList.toggle("active", [
|
|
648
|
+
b.classList.toggle("active", TAB_NAMES[i] === name)
|
|
416
649
|
})
|
|
417
650
|
document.getElementById(`tab-${name}`).classList.add("active")
|
|
418
651
|
activeTab = name
|
|
419
652
|
if (name === "coverage" && !coverageData) loadCoverage()
|
|
653
|
+
if (name === "growth" && !growthData) loadGrowth()
|
|
420
654
|
}
|
|
421
655
|
|
|
422
656
|
// ── Toast ──────────────────────────────────────────────────────────────────
|
|
@@ -541,10 +775,49 @@ async function loadProposals() {
|
|
|
541
775
|
}
|
|
542
776
|
}
|
|
543
777
|
|
|
778
|
+
function juryBreakdown(jury) {
|
|
779
|
+
if (!jury?.breakdown) return ""
|
|
780
|
+
const dims = [
|
|
781
|
+
["Evidence support", jury.breakdown.evidence_support],
|
|
782
|
+
["Feasibility", jury.breakdown.feasibility],
|
|
783
|
+
["Risk", jury.breakdown.risk],
|
|
784
|
+
["Completeness", jury.breakdown.completeness],
|
|
785
|
+
]
|
|
786
|
+
const rows = dims.filter(([,v]) => v != null).map(([label, v]) => {
|
|
787
|
+
const pct = Math.round(v * 100)
|
|
788
|
+
return `<div class="breakdown-row">
|
|
789
|
+
<span class="breakdown-label">${esc(label)}</span>
|
|
790
|
+
<span class="breakdown-bar"><span class="breakdown-fill" style="width:${pct}%"></span></span>
|
|
791
|
+
<span class="breakdown-pct">${pct}%</span>
|
|
792
|
+
</div>`
|
|
793
|
+
}).join("")
|
|
794
|
+
if (!rows) return ""
|
|
795
|
+
return `<div class="jury-section">
|
|
796
|
+
<div class="jury-section-title">Jury · confidence ${Math.round((jury.confidence ?? 0) * 100)}% · ${esc(jury.recommendation ?? "")}</div>
|
|
797
|
+
${rows}
|
|
798
|
+
</div>`
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function councilConditions(council) {
|
|
802
|
+
if (!council?.conditions?.length) return ""
|
|
803
|
+
const items = council.conditions.map(c => `<li>${esc(c)}</li>`).join("")
|
|
804
|
+
const sat = council.satisfied ? "✓ satisfied" : "✗ not satisfied"
|
|
805
|
+
return `<div class="council-section">
|
|
806
|
+
<div class="council-section-title">Council · ${esc(sat)} · ${esc(council.recommendation ?? "")}</div>
|
|
807
|
+
<ul>${items}</ul>
|
|
808
|
+
</div>`
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function evidenceTags(ids) {
|
|
812
|
+
if (!ids?.length) return ""
|
|
813
|
+
const tags = ids.map(id => `<span class="evidence-tag">${esc(String(id).slice(0,8))}</span>`).join("")
|
|
814
|
+
return `<div class="evidence-tags">${tags}</div>`
|
|
815
|
+
}
|
|
816
|
+
|
|
544
817
|
function renderProposals(proposals) {
|
|
545
818
|
const el = document.getElementById("proposalList")
|
|
546
819
|
if (!proposals.length) {
|
|
547
|
-
el.innerHTML = `<div class="empty">No pending proposals<small>AI agents stage proposals via <code>
|
|
820
|
+
el.innerHTML = `<div class="empty">No pending proposals<small>AI agents stage proposals via <code>quorum_stage</code> MCP tool or <code>oracle.propose()</code></small></div>`
|
|
548
821
|
return
|
|
549
822
|
}
|
|
550
823
|
el.innerHTML = proposals.map(p => `
|
|
@@ -555,13 +828,18 @@ function renderProposals(proposals) {
|
|
|
555
828
|
</div>
|
|
556
829
|
<div class="card-body">${esc(p.decision ?? p.key_insight ?? "")}</div>
|
|
557
830
|
${areas(p.affected_areas)}
|
|
831
|
+
${evidenceTags(p.evidence_cited)}
|
|
832
|
+
${juryBreakdown(p.jury)}
|
|
833
|
+
${councilConditions(p.council)}
|
|
558
834
|
<div class="card-meta" style="margin-top:10px">
|
|
559
835
|
<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${esc(p.proposalId?.slice(0,8))}</span>
|
|
560
836
|
${confidenceBar(p.confidence)}
|
|
837
|
+
${p.source_module ? `<span style="font-size:11px;color:var(--muted)">${esc(p.source_module)}</span>` : ""}
|
|
561
838
|
</div>
|
|
562
839
|
<div class="actions">
|
|
563
840
|
<button class="btn btn-approve" onclick="approveProposal('${esc(p.proposalId)}', this)">✓ Approve</button>
|
|
564
841
|
<button class="btn btn-reject" onclick="rejectProposal('${esc(p.proposalId)}', this)">✕ Reject</button>
|
|
842
|
+
<button class="btn" onclick="openEditModal('${esc(p.proposalId)}')">Edit</button>
|
|
565
843
|
</div>
|
|
566
844
|
</div>
|
|
567
845
|
`).join("")
|
|
@@ -603,6 +881,48 @@ async function rejectProposal(id, btn) {
|
|
|
603
881
|
}
|
|
604
882
|
}
|
|
605
883
|
|
|
884
|
+
// ── Edit modal ────────────────────────────────────────────────────────────
|
|
885
|
+
|
|
886
|
+
function openEditModal(id) {
|
|
887
|
+
const p = allProposals.find(x => x.proposalId === id)
|
|
888
|
+
if (!p) return
|
|
889
|
+
document.getElementById("editId").value = id
|
|
890
|
+
document.getElementById("editTopic").value = p.topic ?? ""
|
|
891
|
+
document.getElementById("editDecision").value = p.decision ?? p.key_insight ?? ""
|
|
892
|
+
document.getElementById("editStatus").value = p.status ?? "open"
|
|
893
|
+
document.getElementById("editConfidence").value = p.confidence ?? 0.7
|
|
894
|
+
document.getElementById("editModal").classList.add("open")
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function closeEditModal(e) {
|
|
898
|
+
if (e && e.target !== document.getElementById("editModal")) return
|
|
899
|
+
document.getElementById("editModal").classList.remove("open")
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function saveEdit() {
|
|
903
|
+
const id = document.getElementById("editId").value
|
|
904
|
+
const topic = document.getElementById("editTopic").value.trim()
|
|
905
|
+
const decision = document.getElementById("editDecision").value.trim()
|
|
906
|
+
const status = document.getElementById("editStatus").value
|
|
907
|
+
const confidence = parseFloat(document.getElementById("editConfidence").value)
|
|
908
|
+
|
|
909
|
+
if (!topic || !decision) { toast("Topic and decision are required", "err"); return }
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
const res = await fetch(`/api/proposals/${encodeURIComponent(id)}`, {
|
|
913
|
+
method: "PATCH",
|
|
914
|
+
headers: { "content-type": "application/json" },
|
|
915
|
+
body: JSON.stringify({ topic, decision, key_insight: decision, status, confidence }),
|
|
916
|
+
})
|
|
917
|
+
if (!res.ok) throw new Error((await res.json()).error)
|
|
918
|
+
document.getElementById("editModal").classList.remove("open")
|
|
919
|
+
toast("Proposal updated")
|
|
920
|
+
loadProposals()
|
|
921
|
+
} catch (err) {
|
|
922
|
+
toast(err.message, "err")
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
606
926
|
// ── Coverage ──────────────────────────────────────────────────────────────
|
|
607
927
|
|
|
608
928
|
async function loadCoverage() {
|
|
@@ -671,6 +991,99 @@ function renderCoverage(data) {
|
|
|
671
991
|
${totalFiles === 0 ? `<div class="empty">No source files found</div>` : ""}
|
|
672
992
|
`
|
|
673
993
|
}
|
|
994
|
+
|
|
995
|
+
// ── Growth ────────────────────────────────────────────────────────────────
|
|
996
|
+
|
|
997
|
+
async function loadGrowth() {
|
|
998
|
+
document.getElementById("growthView").innerHTML = `<div class="loading">Loading…</div>`
|
|
999
|
+
try {
|
|
1000
|
+
const res = await fetch("/api/growth")
|
|
1001
|
+
growthData = await res.json()
|
|
1002
|
+
renderGrowth(growthData)
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
document.getElementById("growthView").innerHTML =
|
|
1005
|
+
`<div class="empty">Failed to load growth data<small>${esc(err.message)}</small></div>`
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function renderGrowth(data) {
|
|
1010
|
+
const { health, entries, proposals, hint } = data
|
|
1011
|
+
const col = health >= 80 ? "var(--green)" : health >= 60 ? "var(--accent)" : health >= 40 ? "var(--yellow)" : "var(--red)"
|
|
1012
|
+
const label = health >= 80 ? "THRIVING" : health >= 60 ? "HEALTHY" : health >= 40 ? "SLOW" : "STALLED"
|
|
1013
|
+
|
|
1014
|
+
document.getElementById("growthView").innerHTML = `
|
|
1015
|
+
<div class="health-score-wrap">
|
|
1016
|
+
<div class="health-score-num" style="color:${col}">${health}</div>
|
|
1017
|
+
<div class="health-score-text">
|
|
1018
|
+
<div class="health-score-label" style="color:${col}">${label}</div>
|
|
1019
|
+
<div class="health-hint">${esc(hint ?? "")}</div>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
|
|
1023
|
+
<div class="health-grid">
|
|
1024
|
+
<div class="health-stat">
|
|
1025
|
+
<div class="health-stat-val">${entries.total}</div>
|
|
1026
|
+
<div class="health-stat-lbl">Total entries</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
<div class="health-stat">
|
|
1029
|
+
<div class="health-stat-val" style="color:var(--green)">${entries.byStatus.validated ?? 0}</div>
|
|
1030
|
+
<div class="health-stat-lbl">Validated</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
<div class="health-stat">
|
|
1033
|
+
<div class="health-stat-val" style="color:var(--blue)">${entries.byStatus.open ?? 0}</div>
|
|
1034
|
+
<div class="health-stat-lbl">Open</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
<div class="health-stat">
|
|
1037
|
+
<div class="health-stat-val" style="color:var(--red)">${entries.byStatus.refuted ?? 0}</div>
|
|
1038
|
+
<div class="health-stat-lbl">Refuted</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="health-stat">
|
|
1041
|
+
<div class="health-stat-val" style="color:var(--yellow)">${proposals.pending}</div>
|
|
1042
|
+
<div class="health-stat-lbl">Pending proposals</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
<div class="health-stat">
|
|
1045
|
+
<div class="health-stat-val">${Math.round((entries.avgConfidence ?? 0) * 100)}%</div>
|
|
1046
|
+
<div class="health-stat-lbl">Avg confidence</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
${proposals.pending > 0 ? `
|
|
1051
|
+
<div class="card" style="border-color:rgba(224,185,82,.3);background:rgba(224,185,82,.04)">
|
|
1052
|
+
<div class="card-body" style="color:var(--yellow)">
|
|
1053
|
+
${proposals.pending} proposal${proposals.pending === 1 ? "" : "s"} awaiting approval.
|
|
1054
|
+
<a href="#" onclick="showTab('proposals');return false" style="color:var(--accent);text-decoration:none;margin-left:6px">Review →</a>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>` : ""}
|
|
1057
|
+
`
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ── Compass ───────────────────────────────────────────────────────────────
|
|
1061
|
+
|
|
1062
|
+
async function loadCompass() {
|
|
1063
|
+
const subcommand = document.getElementById("compassSubcmd").value
|
|
1064
|
+
document.getElementById("compassView").innerHTML = `<div class="loading">Running compass ${esc(subcommand)}…</div>`
|
|
1065
|
+
try {
|
|
1066
|
+
const res = await fetch(`/api/compass?subcommand=${encodeURIComponent(subcommand)}`)
|
|
1067
|
+
const data = await res.json()
|
|
1068
|
+
renderCompass(data, subcommand)
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
document.getElementById("compassView").innerHTML =
|
|
1071
|
+
`<div class="empty">Failed to run compass<small>${esc(err.message)}</small></div>`
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function renderCompass(data, subcommand) {
|
|
1076
|
+
const el = document.getElementById("compassView")
|
|
1077
|
+
if (data.status === "no-llm") {
|
|
1078
|
+
el.innerHTML = `<div class="empty">${esc(data.message)}<small>Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY and restart quorum serve.</small></div>`
|
|
1079
|
+
return
|
|
1080
|
+
}
|
|
1081
|
+
const output = data.output ?? JSON.stringify(data, null, 2)
|
|
1082
|
+
el.innerHTML = `
|
|
1083
|
+
<div style="font-size:12px;color:var(--muted);margin-bottom:10px">compass ${esc(subcommand)}</div>
|
|
1084
|
+
<div class="compass-output">${esc(output)}</div>
|
|
1085
|
+
`
|
|
1086
|
+
}
|
|
674
1087
|
</script>
|
|
675
1088
|
</body>
|
|
676
1089
|
</html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lance-db.d.ts","sourceRoot":"","sources":["../../../modules/oracle/adapters/lance-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAa9C,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"lance-db.d.ts","sourceRoot":"","sources":["../../../modules/oracle/adapters/lance-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAa9C,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA0DnF"}
|
|
@@ -64,7 +64,8 @@ export async function createLanceDBStore(chronicleDir) {
|
|
|
64
64
|
const t = await getOrCreateTable();
|
|
65
65
|
if (!t)
|
|
66
66
|
return [];
|
|
67
|
-
|
|
67
|
+
// vectordb v0.4.x has no t.query() — use filter with a tautology to fetch all rows
|
|
68
|
+
const rows = await t.filter("id IS NOT NULL").execute();
|
|
68
69
|
return rows.map(row => JSON.parse(row.payload));
|
|
69
70
|
},
|
|
70
71
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lance-db.js","sourceRoot":"","sources":["../../../modules/oracle/adapters/lance-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,IAAI,MAAM,MAAM,CAAA;AAWvB,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,YAAoB;IAC3D,4EAA4E;IAC5E,sEAAsE;IACtE,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAA;IAC3C,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,IAAI,UAAU,CAAA;IAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;IACnD,+EAA+E;IAC/E,mEAAmE;IACnE,8DAA8D;IAC9D,MAAM,EAAE,GAAQ,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC/C,IAAI,KAAK,GAAQ,IAAI,CAAA;IAErB,KAAK,UAAU,gBAAgB,CAAC,QAAmB;QACjD,IAAI,KAAK;YAAE,OAAO,KAAK,CAAA;QACvB,MAAM,KAAK,GAAa,MAAM,EAAE,CAAC,UAAU,EAAE,CAAA;QAC7C,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,KAAK,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;QACvC,CAAC;aAAM,IAAI,QAAQ,EAAE,CAAC;YACpB,4EAA4E;YAC5E,wEAAwE;YACxE,+EAA+E;YAC/E,KAAK,GAAG,MAAM,EAAE,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;QACrD,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ;YAC/B,MAAM,GAAG,GAAa,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAA;YACvE,MAAM,CAAC,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAA;YACrC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC;gBAChB,0DAA0D;gBAC1D,OAAM;YACR,CAAC;YACD,oEAAoE;YACpE,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,UAAU,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;YAC1C,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACpB,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK;YACxB,MAAM,CAAC,GAAG,MAAM,gBAAgB,EAAE,CAAA;YAClC,IAAI,CAAC,CAAC;gBAAE,OAAO,EAAE,CAAA;YACjB,MAAM,IAAI,GAAe,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAA;YACtE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACtB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAmB;gBAChD,wEAAwE;gBACxE,KAAK,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;aAC3D,CAAC,CAAC,CAAA;QACL,CAAC;QAED,KAAK,CAAC,MAAM;YACV,MAAM,CAAC,GAAG,MAAM,gBAAgB,EAAE,CAAA;YAClC,IAAI,CAAC,CAAC;gBAAE,OAAO,EAAE,CAAA;YACjB,MAAM,IAAI,GAAe,MAAM,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"lance-db.js","sourceRoot":"","sources":["../../../modules/oracle/adapters/lance-db.ts"],"names":[],"mappings":"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"}
|