@balpal4495/quorum 0.3.0 → 0.4.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 +105 -2
- package/SETUP.md +24 -0
- package/bin/commands/check.js +122 -0
- package/bin/commands/commit.js +210 -0
- package/bin/commands/init.js +236 -0
- package/bin/commands/sentinel.js +160 -0
- package/bin/commands/status.js +117 -0
- package/bin/quorum.js +103 -0
- package/bin/shared/chronicle.js +129 -0
- package/bin/shared/colors.js +22 -0
- package/bin/shared/patterns.js +83 -0
- package/package.json +2 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { promises as fs } from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from startDir looking for a .chronicle/ directory.
|
|
6
|
+
* Returns the .chronicle path if found, null otherwise.
|
|
7
|
+
*/
|
|
8
|
+
export async function findChronicleDir(startDir = process.cwd()) {
|
|
9
|
+
let dir = startDir
|
|
10
|
+
while (true) {
|
|
11
|
+
const candidate = path.join(dir, ".chronicle")
|
|
12
|
+
try {
|
|
13
|
+
const stat = await fs.stat(candidate)
|
|
14
|
+
if (stat.isDirectory()) return candidate
|
|
15
|
+
} catch { /* keep walking */ }
|
|
16
|
+
const parent = path.dirname(dir)
|
|
17
|
+
if (parent === dir) return null
|
|
18
|
+
dir = parent
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read all proposal JSON files from .chronicle/proposals/.
|
|
24
|
+
* Returns array of { proposalId, ...entry } objects.
|
|
25
|
+
*/
|
|
26
|
+
export async function readProposals(chronicleDir) {
|
|
27
|
+
const dir = path.join(chronicleDir, "proposals")
|
|
28
|
+
let files
|
|
29
|
+
try { files = await fs.readdir(dir) } catch { return [] }
|
|
30
|
+
const results = []
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
if (!file.endsWith(".json")) continue
|
|
33
|
+
try {
|
|
34
|
+
const raw = await fs.readFile(path.join(dir, file), "utf8")
|
|
35
|
+
results.push({ proposalId: file.replace(".json", ""), ...JSON.parse(raw) })
|
|
36
|
+
} catch { /* skip malformed */ }
|
|
37
|
+
}
|
|
38
|
+
return results
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read all committed Chronicle entries from .chronicle/committed/.
|
|
43
|
+
* Returns ChronicleEntry array sorted newest-first.
|
|
44
|
+
*/
|
|
45
|
+
export async function readCommitted(chronicleDir) {
|
|
46
|
+
const dir = path.join(chronicleDir, "committed")
|
|
47
|
+
let files
|
|
48
|
+
try { files = await fs.readdir(dir) } catch { return [] }
|
|
49
|
+
const results = []
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (!file.endsWith(".json")) continue
|
|
52
|
+
try {
|
|
53
|
+
const raw = await fs.readFile(path.join(dir, file), "utf8")
|
|
54
|
+
results.push(JSON.parse(raw))
|
|
55
|
+
} catch { /* skip malformed */ }
|
|
56
|
+
}
|
|
57
|
+
return results.sort((a, b) => b.timestamp?.localeCompare(a.timestamp ?? "") ?? 0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** entryText mirrors the shared/types.ts entryText helper. */
|
|
61
|
+
export function entryText(entry) {
|
|
62
|
+
return `${entry.key_insight}. ${entry.decision ?? ""}`.trim().replace(/\.\.$/, ".")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Rebuild .chronicle/SUMMARY.md from all committed entries. */
|
|
66
|
+
export async function updateSummary(chronicleDir) {
|
|
67
|
+
const SUMMARY_WEEKS = 12
|
|
68
|
+
const DIRECTIVE =
|
|
69
|
+
"<!-- Chronicle Summary v1 — temporal orientation for agents. " +
|
|
70
|
+
"Use for sequence context; query Oracle by entry ID for full reasoning. -->"
|
|
71
|
+
|
|
72
|
+
function isoWeekKey(date) {
|
|
73
|
+
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
|
|
74
|
+
const day = d.getUTCDay() || 7
|
|
75
|
+
d.setUTCDate(d.getUTCDate() + 4 - day)
|
|
76
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
|
77
|
+
const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7)
|
|
78
|
+
return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function workRefLabel(entry) {
|
|
82
|
+
if (!entry.work_ref) return "__none__"
|
|
83
|
+
const { type, ref } = entry.work_ref
|
|
84
|
+
return ref ? `[${type} ${ref}]` : `[${type}]`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderEntry(entry) {
|
|
88
|
+
const areas = entry.affected_areas.join(", ")
|
|
89
|
+
const id = entry.id.slice(0, 8)
|
|
90
|
+
return `- **[${id}]** ${areas} — \`${entry.status}\` (${entry.confidence.toFixed(2)}) — ${entryText(entry)}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const entries = await readCommitted(chronicleDir)
|
|
94
|
+
if (entries.length === 0) return
|
|
95
|
+
|
|
96
|
+
const byWeek = new Map()
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const week = isoWeekKey(new Date(entry.timestamp))
|
|
99
|
+
const bucket = byWeek.get(week) ?? []
|
|
100
|
+
bucket.push(entry)
|
|
101
|
+
byWeek.set(week, bucket)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const weeks = [...byWeek.keys()].sort().reverse().slice(0, SUMMARY_WEEKS)
|
|
105
|
+
const lines = [DIRECTIVE, ""]
|
|
106
|
+
|
|
107
|
+
for (const week of weeks) {
|
|
108
|
+
lines.push(`## Week ${week}`, "")
|
|
109
|
+
const weekEntries = byWeek.get(week)
|
|
110
|
+
const byWork = new Map()
|
|
111
|
+
for (const entry of weekEntries) {
|
|
112
|
+
const key = workRefLabel(entry)
|
|
113
|
+
const bucket = byWork.get(key) ?? []
|
|
114
|
+
bucket.push(entry)
|
|
115
|
+
byWork.set(key, bucket)
|
|
116
|
+
}
|
|
117
|
+
const workKeys = [...byWork.keys()].sort((a, b) =>
|
|
118
|
+
a === "__none__" ? 1 : b === "__none__" ? -1 : a.localeCompare(b))
|
|
119
|
+
for (const key of workKeys) {
|
|
120
|
+
lines.push(key === "__none__"
|
|
121
|
+
? `### (no work context — query Oracle by entry ID for details)`
|
|
122
|
+
: `### ${key}`)
|
|
123
|
+
for (const entry of byWork.get(key)) lines.push(renderEntry(entry))
|
|
124
|
+
lines.push("")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await fs.writeFile(path.join(chronicleDir, "SUMMARY.md"), lines.join("\n"), "utf8")
|
|
129
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export const c = {
|
|
3
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
4
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
5
|
+
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
6
|
+
dim: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
7
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
8
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
9
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
10
|
+
magenta:(s) => `\x1b[35m${s}\x1b[0m`,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const log = {
|
|
14
|
+
section: (title) => console.log(`\n${c.bold(title)}`),
|
|
15
|
+
created: (file) => console.log(` ${c.green("+ created ")} ${file}`),
|
|
16
|
+
appended:(file) => console.log(` ${c.blue("~ appended")} ${file}`),
|
|
17
|
+
skipped: (file) => console.log(` ${c.dim("· skipped ")} ${file}`),
|
|
18
|
+
warn: (msg) => console.log(` ${c.yellow("⚠ " + msg)}`),
|
|
19
|
+
ok: (msg) => console.log(` ${c.green("✓")} ${msg}`),
|
|
20
|
+
fail: (msg) => console.log(` ${c.red("✗")} ${msg}`),
|
|
21
|
+
info: (msg) => console.log(` ${c.dim(msg)}`),
|
|
22
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors modules/jury/preflight.ts (SENSITIVE_PATTERNS, ROLLBACK_PATTERN, TEST_PATTERN)
|
|
3
|
+
* and modules/council/risk.ts (RISK_RULES).
|
|
4
|
+
* Keep these in sync when modifying those files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const SENSITIVE_PATTERNS = {
|
|
8
|
+
auth: /\b(auth(?:entication|orization)?|jwt|token|session|password|oauth|login|logout|credential|bearer)\b/i,
|
|
9
|
+
database: /\b(migrat(?:ion|e)|alter\s+table|schema\s+change|postgres|mysql|sqlite|prisma|drizzle|knex|sequelize)\b/i,
|
|
10
|
+
crypto: /\b(encrypt|decrypt|cipher|hash(?:ing)?|hmac|sign(?:ing)?|verify|private\s+key|certificate|tls|ssl)\b/i,
|
|
11
|
+
payments: /\b(payment|stripe|charge|billing|invoice|subscription|price|checkout|refund)\b/i,
|
|
12
|
+
permissions: /\b(permission|role(?:s)?|acl|access\s+control|rbac|authorization|entitlement)\b/i,
|
|
13
|
+
pii: /\b(pii|personal\s+data|gdpr|ccpa|email(?:\s+address)?|phone(?:\s+number)?|postal\s+address|ssn|passport)\b/i,
|
|
14
|
+
data_deletion: /\b(delete(?:\s+all)?|drop\s+table|truncate|purge|wipe|destroy.*data|hard\s+delete)\b/i,
|
|
15
|
+
secrets: /\b(api\s+key|secret(?:s)?|env(?:ironment)?\s+var(?:iable)?|\.env|private\s+key|credentials?)\b/i,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ROLLBACK_PATTERN = /\b(rollback|roll\s+back|revert|undo|restore|recovery|fallback|backward[- ]compat)\b/i
|
|
19
|
+
export const TEST_PATTERN = /\b(test(?:ing|s)?|spec(?:ification)?|unit\s+test|integration\s+test|coverage|vitest|jest|mocha)\b/i
|
|
20
|
+
|
|
21
|
+
const RISK_RULES = [
|
|
22
|
+
// Critical
|
|
23
|
+
{ pattern: /\b(auth(?:entication|orization)?|jwt|token|session|password|oauth|credential|bearer)\b/i, level: "critical", reason: "authentication or authorisation logic" },
|
|
24
|
+
{ pattern: /\b(payment|stripe|charge|billing|checkout|refund|subscription)\b/i, level: "critical", reason: "payment or billing logic" },
|
|
25
|
+
{ pattern: /\b(encrypt|decrypt|private\s+key|certificate|tls|ssl|hmac|cipher)\b/i, level: "critical", reason: "cryptography or key management" },
|
|
26
|
+
{ pattern: /\b(delete\s+all|drop\s+table|truncate|wipe|destroy.*data|hard\s+delete)\b/i, level: "critical", reason: "irreversible data deletion" },
|
|
27
|
+
// High
|
|
28
|
+
{ pattern: /\b(migrat(?:ion|e)|alter\s+table|schema\s+change|not\s+null|backfill|pg_repack|shadow\s+column)\b/i, level: "high", reason: "database schema migration" },
|
|
29
|
+
{ pattern: /\b(permission|role(?:s)?|acl|rbac|access\s+control|entitlement)\b/i, level: "high", reason: "permissions or access control" },
|
|
30
|
+
{ pattern: /\b(pii|personal\s+data|gdpr|ccpa|email(?:\s+address)?|phone(?:\s+number)?|ssn|passport)\b/i, level: "high", reason: "PII or compliance-regulated data" },
|
|
31
|
+
{ pattern: /\b(api\s+key|secret(?:s)?|private\s+key|credentials?)\b/i, level: "high", reason: "secrets or credentials handling" },
|
|
32
|
+
// Medium
|
|
33
|
+
{ pattern: /\b(cache|redis|memcached|invalidat(?:e|ion))\b/i, level: "medium", reason: "cache strategy" },
|
|
34
|
+
{ pattern: /\b(rate\s*limit|throttl(?:e|ing)|quota)\b/i, level: "medium", reason: "rate limiting or throttling" },
|
|
35
|
+
{ pattern: /\b(webhook|event|queue|pubsub|kafka|rabbitmq|sns|sqs)\b/i, level: "medium", reason: "async event or messaging" },
|
|
36
|
+
{ pattern: /\b(deploy(?:ment)?|ci(?:\/cd)?|docker|kubernetes|infra(?:structure)?)\b/i, level: "medium", reason: "deployment or infrastructure" },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const RISK_ORDER = ["low", "medium", "high", "critical"]
|
|
40
|
+
|
|
41
|
+
function maxLevel(a, b) {
|
|
42
|
+
return RISK_ORDER.indexOf(a) >= RISK_ORDER.indexOf(b) ? a : b
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function runPreflight(outcome, design) {
|
|
46
|
+
const text = `${outcome} ${design}`
|
|
47
|
+
const sensitive_areas = Object.entries(SENSITIVE_PATTERNS)
|
|
48
|
+
.filter(([, pattern]) => pattern.test(text))
|
|
49
|
+
.map(([area]) => area)
|
|
50
|
+
return {
|
|
51
|
+
touches_sensitive_area: sensitive_areas.length > 0,
|
|
52
|
+
sensitive_areas,
|
|
53
|
+
rollback_mentioned: ROLLBACK_PATTERN.test(text),
|
|
54
|
+
test_strategy_mentioned: TEST_PATTERN.test(text),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function classifyRisk(outcome, design, refutedCount = 0) {
|
|
59
|
+
const text = `${outcome} ${design}`
|
|
60
|
+
let level = "low"
|
|
61
|
+
const reasons = []
|
|
62
|
+
|
|
63
|
+
for (const rule of RISK_RULES) {
|
|
64
|
+
if (rule.pattern.test(text)) {
|
|
65
|
+
const prev = level
|
|
66
|
+
level = maxLevel(level, rule.level)
|
|
67
|
+
if (!reasons.includes(rule.reason)) reasons.push(rule.reason)
|
|
68
|
+
void prev
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (refutedCount > 0) {
|
|
73
|
+
const refutedRisk = refutedCount >= 2 ? "high" : "medium"
|
|
74
|
+
level = maxLevel(level, refutedRisk)
|
|
75
|
+
reasons.push(`${refutedCount} refuted Chronicle ${refutedCount === 1 ? "entry" : "entries"} in evidence pack`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
level,
|
|
80
|
+
reasons: reasons.length > 0 ? reasons : ["no sensitive patterns detected"],
|
|
81
|
+
council_mode: level === "low" ? "jury-only" : level === "medium" ? "lite" : "full",
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@balpal4495/quorum",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Portable reasoning layer for agentic codebases — Oracle, Jury, Council, Sentinel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"llm"
|
|
21
21
|
],
|
|
22
22
|
"bin": {
|
|
23
|
-
"quorum": "bin/
|
|
23
|
+
"quorum": "bin/quorum.js"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"init": "node bin/init.js",
|