@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.
- package/bin/commands/compass.js +139 -77
- package/package.json +1 -1
package/bin/commands/compass.js
CHANGED
|
@@ -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 = ["
|
|
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
|
|
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() && !
|
|
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
|
-
|
|
115
|
-
|
|
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 =>
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
for (const f of
|
|
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
|
-
|
|
229
|
-
for (const f of
|
|
230
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
|
275
|
-
(d.user_problem_clarity
|
|
276
|
-
(d.evidence_strength
|
|
277
|
-
(d.leverage
|
|
278
|
-
(d.feasibility
|
|
279
|
-
(d.time_to_signal
|
|
280
|
-
(d.reversibility
|
|
281
|
-
(d.complexity_penalty
|
|
282
|
-
(d.dependency_penalty
|
|
283
|
-
(d.contradiction_penalty
|
|
284
|
-
(d.evidence_gap_penalty
|
|
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
|
-
|
|
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) {
|