@balpal4495/quorum 3.1.0 → 3.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.
@@ -74,7 +74,7 @@ function formatBearings(bearings) {
74
74
  function inferTags(text) {
75
75
  const tags = []
76
76
  const lower = text.toLowerCase()
77
- const keywords = ["oracle","advisor","jury","council","sentinel","compass","cli","api","auth","test","docs","config","chronicle","llm","module"]
77
+ const keywords = ["cli","api","auth","test","docs","config","llm","module","route","page","component","schema","database","db","webhook","deploy","ui","middleware","service"]
78
78
  for (const kw of keywords) if (lower.includes(kw)) tags.push(kw)
79
79
  return tags
80
80
  }
@@ -82,7 +82,7 @@ function inferTags(text) {
82
82
  async function scanDocs(rootDir, area) {
83
83
  const findings = []
84
84
  let idx = 0
85
- const targets = ["README.md","SETUP.md","CLAUDE.md","AGENTS.md","modules/README.md","quorum/CLAUDE.md","docs"]
85
+ const SKIP_DIRS = new Set(["node_modules",".git","dist","build","out",".next",".chronicle","coverage",".cache",".turbo",".vercel"])
86
86
  async function scanMd(filePath) {
87
87
  let content
88
88
  try { content = await fs.readFile(filePath, "utf8") } catch { return }
@@ -96,10 +96,6 @@ async function scanDocs(rootDir, area) {
96
96
  const context = lines.slice(i + 1, i + 4).join(" ").replace(/```[^`]*```/g, "").trim().slice(0, 200)
97
97
  findings.push({ id: `docs-${idx++}`, kind: "docs", source: rel, path: rel, line: i + 1, title: heading, summary: context || heading, confidence: 0.8, tags: inferTags(heading + " " + context) })
98
98
  }
99
- const trimmed = line.trim()
100
- if (trimmed.startsWith("quorum ") || trimmed.startsWith("npx quorum")) {
101
- findings.push({ id: `docs-cmd-${idx++}`, kind: "docs", source: rel, path: rel, line: i + 1, title: `CLI usage: ${trimmed.slice(0, 60)}`, summary: `Documented command: ${trimmed}`, confidence: 0.85, tags: ["cli", "command", ...inferTags(trimmed)] })
102
- }
103
99
  }
104
100
  }
105
101
  async function scanDir(dir) {
@@ -107,18 +103,24 @@ async function scanDocs(rootDir, area) {
107
103
  try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return }
108
104
  for (const entry of entries) {
109
105
  const full = path.join(dir, entry.name)
110
- if (entry.isDirectory() && !["node_modules",".git","dist"].includes(entry.name)) await scanDir(full)
106
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) await scanDir(full)
111
107
  else if (entry.isFile() && entry.name.endsWith(".md")) await scanMd(full)
112
108
  }
113
109
  }
114
- for (const target of targets) {
115
- const full = path.join(rootDir, target)
110
+ // Scan all .md files at project root (non-recursive)
111
+ let rootEntries
112
+ try { rootEntries = await fs.readdir(rootDir, { withFileTypes: true }) } catch { rootEntries = [] }
113
+ for (const entry of rootEntries) {
114
+ if (entry.isFile() && entry.name.endsWith(".md")) await scanMd(path.join(rootDir, entry.name))
115
+ }
116
+ // Scan standard documentation directories
117
+ for (const d of ["docs","documentation","doc",".github","wiki"]) {
118
+ const full = path.join(rootDir, d)
116
119
  let stat
117
120
  try { stat = await fs.stat(full) } catch { continue }
118
121
  if (stat.isDirectory()) await scanDir(full)
119
- else await scanMd(full)
120
122
  }
121
- return area ? findings.filter(f => !area || f.tags.includes(area.toLowerCase()) || f.summary.toLowerCase().includes(area.toLowerCase())) : findings
123
+ return area ? findings.filter(f => f.tags.includes(area.toLowerCase()) || f.summary.toLowerCase().includes(area.toLowerCase())) : findings
122
124
  }
123
125
 
124
126
  async function scanPackage(rootDir) {
@@ -138,24 +140,59 @@ async function scanPackage(rootDir) {
138
140
  async function scanCli(rootDir, area) {
139
141
  const findings = []
140
142
  let idx = 0
141
- const binDir = path.join(rootDir, "bin", "commands")
142
- let files
143
- try { files = (await fs.readdir(binDir)).filter(f => f.endsWith(".js")) } catch { return findings }
144
- for (const file of files) {
145
- const cmdName = file.replace(".js", "")
146
- let content
147
- try { content = await fs.readFile(path.join(binDir, file), "utf8") } catch { continue }
148
- const rel = `bin/commands/${file}`
149
- const subcmds = [...content.matchAll(/case ["']([a-z-]+)["']/g)].map(m => m[1])
150
- const flags = [...new Set([...content.matchAll(/["'](--[a-z-]+)["']/g)].map(m => m[1]))]
151
- const usesLLM = /llm|LLM|provider|model/.test(content)
152
- const readsChronicle = /readCommitted|findChronicleDir|committed/.test(content)
153
- findings.push({
154
- id: `cli-${idx++}`, kind: "cli", source: rel, path: rel, title: `Command: quorum ${cmdName}`,
155
- summary: [`quorum ${cmdName}`, subcmds.length ? `Subcommands: ${subcmds.join(", ")}` : "", flags.length ? `Flags: ${flags.slice(0,8).join(", ")}` : "", usesLLM ? "Uses LLM" : "No LLM", readsChronicle ? "Reads Chronicle" : ""].filter(Boolean).join(" | "),
156
- confidence: 0.9,
157
- tags: ["cli","command",cmdName,...subcmds.map(s => `subcommand:${s}`), usesLLM ? "llm" : "deterministic"].filter(Boolean),
158
- })
143
+ // ── CLI tool pattern: look for command files in known CLI layouts ──────────
144
+ for (const base of ["bin/commands", "bin", "src/commands", "src/cli"]) {
145
+ const binDir = path.join(rootDir, base)
146
+ let entries
147
+ try { entries = await fs.readdir(binDir, { withFileTypes: true }) } catch { continue }
148
+ const files = entries.filter(e => e.isFile() && /\.(js|ts)$/.test(e.name))
149
+ for (const entry of files) {
150
+ let content
151
+ try { content = await fs.readFile(path.join(binDir, entry.name), "utf8") } catch { continue }
152
+ const cmdName = entry.name.replace(/\.(js|ts)$/, "")
153
+ const fileRel = `${base}/${entry.name}`
154
+ const subcmds = [...content.matchAll(/case ["']([a-z-]+)["']/g)].map(m => m[1])
155
+ const flags = [...new Set([...content.matchAll(/["'](--[a-z-]+)["']/g)].map(m => m[1]))]
156
+ findings.push({
157
+ id: `cli-${idx++}`, kind: "cli", source: fileRel, path: fileRel,
158
+ title: `Command: ${cmdName}`,
159
+ summary: [cmdName, subcmds.length ? `Subcommands: ${subcmds.join(", ")}` : "", flags.length ? `Flags: ${flags.slice(0,8).join(", ")}` : ""].filter(Boolean).join(" | "),
160
+ confidence: 0.9,
161
+ tags: ["cli","command",cmdName,...subcmds.map(s => `subcommand:${s}`)].filter(Boolean),
162
+ })
163
+ }
164
+ if (findings.length) break
165
+ }
166
+ // ── Web app route pattern: Next.js app router or pages router ─────────────
167
+ if (!findings.length) {
168
+ const SKIP = new Set(["node_modules","_components","_lib","_hooks"])
169
+ async function walkRoutes(dir, prefix, isApiBase) {
170
+ let entries
171
+ try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return }
172
+ for (const e of entries) {
173
+ if (SKIP.has(e.name) || e.name.startsWith(".")) continue
174
+ const full = path.join(dir, e.name)
175
+ const rel = path.relative(rootDir, full).replace(/\\/g, "/")
176
+ if (e.isDirectory()) {
177
+ const seg = e.name.startsWith("(") ? "" : ("/" + e.name)
178
+ await walkRoutes(full, prefix + seg, isApiBase || e.name === "api")
179
+ } else if (e.isFile() && /\.(tsx?|jsx?)$/.test(e.name)) {
180
+ const name = e.name.replace(/\.(tsx?|jsx?)$/, "")
181
+ const isPage = ["page","index"].includes(name) && !["_app","_document","_error"].includes(name)
182
+ const isRoute = name === "route"
183
+ if (!isPage && !isRoute) continue
184
+ const route = prefix || "/"
185
+ const isApi = isApiBase || isRoute || prefix.startsWith("/api")
186
+ findings.push({ id: `route-${idx++}`, kind: "route", source: rel, path: rel, title: isApi ? `API: ${route}` : `Page: ${route}`, summary: isApi ? `API route at ${route}` : `Page/screen at ${route}`, confidence: 0.85, tags: [isApi ? "api" : "page", "route", ...inferTags(route + " " + rel)] })
187
+ }
188
+ }
189
+ }
190
+ for (const base of ["app","pages","src/app","src/pages"]) {
191
+ const routeBase = path.join(rootDir, base)
192
+ try { await fs.stat(routeBase) } catch { continue }
193
+ await walkRoutes(routeBase, "", base.includes("api"))
194
+ if (findings.length) break
195
+ }
159
196
  }
160
197
  return area ? findings.filter(f => f.tags.includes(area.toLowerCase()) || f.summary.toLowerCase().includes(area.toLowerCase())) : findings
161
198
  }
@@ -163,15 +200,21 @@ async function scanCli(rootDir, area) {
163
200
  async function scanRepo(rootDir) {
164
201
  const findings = []
165
202
  let idx = 0
166
- const modulesDir = path.join(rootDir, "modules")
167
- try {
168
- const entries = await fs.readdir(modulesDir, { withFileTypes: true })
169
- for (const entry of entries) {
170
- if (entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== "shared") {
171
- findings.push({ id: `repo-module-${idx++}`, kind: "code", source: `modules/${entry.name}/`, path: `modules/${entry.name}/`, title: `Module: ${entry.name}`, summary: `TypeScript module: modules/${entry.name}/`, confidence: 0.85, tags: ["module", entry.name, "code"] })
172
- }
173
- }
174
- } catch { /* no modules dir */ }
203
+ const SKIP = new Set(["node_modules",".git","dist","build","out",".next",".chronicle","coverage",".cache",".turbo",".vercel","public","static","assets","images","fonts"])
204
+ const SOURCE_HINTS = new Set(["src","lib","app","modules","packages","services","components","api","server","client","core","shared","common","utils","hooks","stores","models","types","schemas","db","database"])
205
+ let entries
206
+ try { entries = await fs.readdir(rootDir, { withFileTypes: true }) } catch { return findings }
207
+ for (const entry of entries) {
208
+ if (!entry.isDirectory() || SKIP.has(entry.name) || entry.name.startsWith(".")) continue
209
+ const isSource = SOURCE_HINTS.has(entry.name)
210
+ let subEntries = []
211
+ try { subEntries = await fs.readdir(path.join(rootDir, entry.name), { withFileTypes: true }) } catch {}
212
+ const subDirs = subEntries.filter(e => e.isDirectory() && !SKIP.has(e.name) && !e.name.startsWith(".")).map(e => e.name)
213
+ const fileCount = subEntries.filter(e => e.isFile()).length
214
+ if (fileCount === 0 && subDirs.length === 0) continue
215
+ const desc = subDirs.length ? `Contains: ${subDirs.slice(0, 6).join(", ")}${subDirs.length > 6 ? "\u2026" : ""}` : `${fileCount} files`
216
+ findings.push({ id: `repo-${idx++}`, kind: "code", source: `${entry.name}/`, path: `${entry.name}/`, title: `${isSource ? "Source" : "Directory"}: ${entry.name}/`, summary: desc, confidence: isSource ? 0.9 : 0.7, tags: ["code", entry.name, isSource ? "source" : "directory", ...inferTags(entry.name + " " + desc)] })
217
+ }
175
218
  return findings
176
219
  }
177
220
 
@@ -200,16 +243,14 @@ function formatTerrain(findings, limit = 40) {
200
243
  // ── Behaviour mapping ─────────────────────────────────────────────────────────
201
244
 
202
245
  function inferArea(f) {
203
- const lower = (f.title + " " + f.summary).toLowerCase()
204
- for (const area of ["oracle","advisor","jury","council","sentinel","compass","chronicle","onboarding"]) {
205
- if (lower.includes(area)) return area
246
+ // For routes, derive area from route path segments
247
+ if (f.kind === "route" && f.path) {
248
+ const parts = f.path.split("/").filter(p => p && !p.startsWith("(") && !p.startsWith("[") && !["app","pages","src","route","page","index"].includes(p))
249
+ if (parts.length) return parts[0]
206
250
  }
207
- return f.tags?.find(t => !["cli","command","llm","deterministic","chronicle","module","code"].includes(t)) ?? "general"
208
- }
209
-
210
- function extractCommand(text) {
211
- const m = text.match(/quorum\s+(\w+)/)
212
- return m ? m[1] : ""
251
+ // Fall back to first meaningful tag
252
+ const SKIP = new Set(["cli","command","llm","deterministic","code","source","directory","module","docs","api","route","page","package","identity","description","binary","exports","dependencies","test"])
253
+ return f.tags?.find(t => !SKIP.has(t)) ?? "general"
213
254
  }
214
255
 
215
256
  function findingToRef(f) {
@@ -218,32 +259,34 @@ function findingToRef(f) {
218
259
 
219
260
  function mapBehaviors(findings, area) {
220
261
  const behaviors = []
221
- const gaps = []
222
262
 
223
- const cliFindings = findings.filter(f => f.kind === "cli")
224
- for (const f of cliFindings) {
263
+ // CLI command behaviors
264
+ for (const f of findings.filter(f => f.kind === "cli")) {
225
265
  behaviors.push({ id: `behavior-cli-${f.id}`, area: inferArea(f), name: f.title, current_behavior: f.summary, evidence: [findingToRef(f)], confidence: f.confidence })
226
266
  }
227
267
 
228
- const docsCliFindings = findings.filter(f => f.kind === "docs" && f.tags?.includes("cli"))
229
- for (const f of docsCliFindings) {
230
- const cmd = extractCommand(f.summary)
231
- const alreadyPresent = cmd.length > 3 && behaviors.some(b => b.current_behavior.toLowerCase().includes(cmd.toLowerCase()))
232
- if (!alreadyPresent && cmd) {
233
- behaviors.push({ id: `behavior-docs-${f.id}`, area: inferArea(f), name: `Documented: ${f.title}`, current_behavior: f.summary, evidence: [findingToRef(f)], confidence: f.confidence * 0.9 })
234
- }
268
+ // Web route behaviors
269
+ for (const f of findings.filter(f => f.kind === "route")) {
270
+ behaviors.push({ id: `behavior-route-${f.id}`, area: inferArea(f), name: f.title, current_behavior: f.summary, evidence: [findingToRef(f)], confidence: f.confidence })
235
271
  }
236
272
 
237
- const EXPECTED = ["onboarding","chronicle","advisor","review"]
238
- for (const expected of EXPECTED) {
239
- const has = behaviors.some(b => b.area === expected || b.name.toLowerCase().includes(expected))
240
- if (!has) {
241
- gaps.push({ id: `gap-${expected}`, area: expected, gap: `No first-class CLI command found for '${expected}'.`, why_it_matters: `'${expected}' appears in product docs but has no dedicated CLI surface.`, confidence: 0.7 })
242
- }
273
+ // Documented feature headings (deduplicated, limited to avoid noise)
274
+ const codeNames = new Set(behaviors.map(b => b.name.toLowerCase()))
275
+ for (const f of findings.filter(f => f.kind === "docs").slice(0, 20)) {
276
+ const titleLower = f.title.toLowerCase()
277
+ if ([...codeNames].some(n => n.includes(titleLower.slice(0, 15)) || titleLower.includes(n.slice(0, 15)))) continue
278
+ behaviors.push({ id: `behavior-docs-${f.id}`, area: inferArea(f), name: f.title, current_behavior: f.summary, evidence: [findingToRef(f)], confidence: f.confidence * 0.7 })
279
+ codeNames.add(titleLower)
243
280
  }
244
281
 
245
- if (!behaviors.some(b => b.name.toLowerCase().includes("compass"))) {
246
- gaps.push({ id: "gap-product-direction", area: "product direction", gap: "No product behaviour mapping or direction module currently exists.", why_it_matters: "Quorum helps agents avoid repeating engineering mistakes, but has no module to help avoid repeating product-direction mistakes.", confidence: 0.93 })
282
+ // Gaps: documented areas with no code artifact
283
+ const gaps = []
284
+ const codeAreas = new Set(behaviors.filter(b => ["cli","route","code"].includes(b.evidence[0]?.kind)).map(b => b.area))
285
+ const docOnlyAreas = behaviors.filter(b => b.evidence[0]?.kind === "docs").map(b => b.area)
286
+ for (const docArea of new Set(docOnlyAreas)) {
287
+ if (!codeAreas.has(docArea) && docArea !== "general") {
288
+ gaps.push({ id: `gap-${docArea}`, area: docArea, gap: `'${docArea}' is documented but no corresponding route, command, or source module was found.`, why_it_matters: `May indicate planned-but-unbuilt functionality.`, confidence: 0.6 })
289
+ }
247
290
  }
248
291
 
249
292
  const filtered = area ? behaviors.filter(b => b.area.toLowerCase().includes(area.toLowerCase()) || b.name.toLowerCase().includes(area.toLowerCase())) : behaviors
@@ -270,18 +313,19 @@ function summarizeBehaviorMap(map) {
270
313
 
271
314
  function computeScore(dims) {
272
315
  const d = dims ?? {}
316
+ const n = (v) => { const f = parseFloat(v); return isNaN(f) ? 0 : f }
273
317
  const raw =
274
- (d.strategic_fit ?? 0) * 20 +
275
- (d.user_problem_clarity ?? 0) * 15 +
276
- (d.evidence_strength ?? 0) * 20 +
277
- (d.leverage ?? 0) * 10 +
278
- (d.feasibility ?? 0) * 15 +
279
- (d.time_to_signal ?? 0) * 10 +
280
- (d.reversibility ?? 0) * 10 -
281
- (d.complexity_penalty ?? 0) * 10 -
282
- (d.dependency_penalty ?? 0) * 8 -
283
- (d.contradiction_penalty ?? 0) * 15 -
284
- (d.evidence_gap_penalty ?? 0) * 12
318
+ n(d.strategic_fit) * 20 +
319
+ n(d.user_problem_clarity) * 15 +
320
+ n(d.evidence_strength) * 20 +
321
+ n(d.leverage) * 10 +
322
+ n(d.feasibility) * 15 +
323
+ n(d.time_to_signal) * 10 +
324
+ n(d.reversibility) * 10 -
325
+ n(d.complexity_penalty) * 10 -
326
+ n(d.dependency_penalty) * 8 -
327
+ n(d.contradiction_penalty) * 15 -
328
+ n(d.evidence_gap_penalty) * 12
285
329
  return { ...d, total: Math.max(0, Math.min(100, Math.round(raw))) }
286
330
  }
287
331
 
@@ -393,7 +437,25 @@ Score total = strategic_fit*20 + user_problem_clarity*15 + evidence_strength*20
393
437
 
394
438
  function parseLLMJson(raw) {
395
439
  const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim()
396
- return JSON.parse(cleaned)
440
+ try {
441
+ return JSON.parse(cleaned)
442
+ } catch (firstErr) {
443
+ // Salvage truncated responses: trim to last complete object boundary
444
+ const lastBrace = cleaned.lastIndexOf('},')
445
+ if (lastBrace !== -1) {
446
+ const salvaged = cleaned.slice(0, lastBrace + 1)
447
+ // Find the outermost container and close it
448
+ const openBracket = salvaged.indexOf('[')
449
+ const openBrace = salvaged.indexOf('{')
450
+ try {
451
+ if (openBracket !== -1 && (openBrace === -1 || openBracket < openBrace)) {
452
+ return JSON.parse(salvaged + ']}')
453
+ }
454
+ return JSON.parse(salvaged + '}')
455
+ } catch { /* fall through to original error */ }
456
+ }
457
+ throw firstErr
458
+ }
397
459
  }
398
460
 
399
461
  async function callLLM(llm, userPrompt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balpal4495/quorum",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Git-backed memory and design review for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",