@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.
@@ -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.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/init.js"
23
+ "quorum": "bin/quorum.js"
24
24
  },
25
25
  "scripts": {
26
26
  "init": "node bin/init.js",