@balpal4495/quorum 0.2.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 +223 -11
- package/SETUP.md +30 -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/evals/__tests__/eval.test.ts +31 -0
- package/evals/cases/auth_hs256_rejected.json +46 -0
- package/evals/cases/auth_rs256_valid.json +30 -0
- package/evals/cases/cache_missing_lock.json +31 -0
- package/evals/cases/db_naive_not_null.json +32 -0
- package/evals/cases/logging_pii_leak.json +32 -0
- package/evals/cases/migration_with_rollback.json +43 -0
- package/evals/cases/no_evidence_novel_design.json +16 -0
- package/evals/cases/payment_no_idempotency.json +33 -0
- package/evals/cases/redis_session_rejected.json +32 -0
- package/evals/cases/safe_refactor.json +17 -0
- package/evals/runner.ts +226 -0
- package/modules/AGENTS.md +9 -5
- package/modules/CLAUDE.md +25 -2
- package/modules/README.md +153 -6
- package/modules/council/chairman.ts +84 -14
- package/modules/council/deliberate.ts +24 -4
- package/modules/council/index.ts +6 -1
- package/modules/council/risk.ts +89 -0
- package/modules/council/types.ts +63 -1
- package/modules/jury/evaluate.ts +32 -8
- package/modules/jury/index.ts +3 -1
- package/modules/jury/preflight.ts +101 -0
- package/modules/jury/schema.ts +9 -0
- package/modules/jury/types.ts +20 -1
- package/modules/shared/types.ts +8 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eval suite — runs all cases from evals/cases/ through deterministic checks.
|
|
3
|
+
*
|
|
4
|
+
* Deterministic assertions (preflight, risk classifier) run on every CI pass.
|
|
5
|
+
* LLM-dependent assertions (jury confidence, council recommendation) are skipped
|
|
6
|
+
* unless EVAL_LLM env var is set — they are too slow and costly for standard CI.
|
|
7
|
+
*
|
|
8
|
+
* To run with a real LLM locally:
|
|
9
|
+
* EVAL_LLM=1 OPENAI_API_KEY=sk-... npx vitest run evals/
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect } from "vitest"
|
|
12
|
+
import path from "path"
|
|
13
|
+
import { loadCases, runCase } from "../runner"
|
|
14
|
+
|
|
15
|
+
describe("eval suite — deterministic checks", async () => {
|
|
16
|
+
const cases = await loadCases(path.join(__dirname, "../cases"))
|
|
17
|
+
|
|
18
|
+
for (const evalCase of cases) {
|
|
19
|
+
it(`[${evalCase.id}] ${evalCase.description}`, async () => {
|
|
20
|
+
// No LLM — only runs deterministic assertions (preflight + risk classifier)
|
|
21
|
+
const result = await runCase(evalCase)
|
|
22
|
+
|
|
23
|
+
if (!result.passed) {
|
|
24
|
+
// Surface all failures clearly
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Eval case "${evalCase.id}" failed:\n${result.failures.map(f => ` • ${f}`).join("\n")}`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "auth_hs256_rejected",
|
|
3
|
+
"description": "Proposing HS256 JWT when RS256 was already chosen — should block",
|
|
4
|
+
"outcome": "Add JWT authentication to the API",
|
|
5
|
+
"design": "Use HS256 symmetric JWT tokens with a shared secret stored in environment variables",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "auth-022",
|
|
9
|
+
"key_insight": "HS256 JWT rejected — no way to rotate keys without invalidating all active sessions",
|
|
10
|
+
"decision": "HS256 JWT rejected — no way to rotate keys without invalidating all active sessions. Use RS256.",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["src/auth/", "src/middleware/"],
|
|
13
|
+
"scope": ["auth", "sessions"],
|
|
14
|
+
"status": "refuted",
|
|
15
|
+
"confidence": 0.91,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-03-01T09:00:00Z"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "auth-031",
|
|
22
|
+
"key_insight": "RS256 with short-lived tokens and refresh rotation in httpOnly cookies is the approved pattern",
|
|
23
|
+
"decision": "RS256 with 15-min access tokens and refresh rotation stored in httpOnly cookies is the approved auth pattern",
|
|
24
|
+
"schema_version": 2,
|
|
25
|
+
"affected_areas": ["src/auth/", "src/middleware/"],
|
|
26
|
+
"scope": ["auth", "sessions"],
|
|
27
|
+
"status": "validated",
|
|
28
|
+
"confidence": 0.88,
|
|
29
|
+
"source_module": "council",
|
|
30
|
+
"evidence_cited": ["auth-022"],
|
|
31
|
+
"timestamp": "2025-03-15T11:00:00Z"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"expected": {
|
|
35
|
+
"jury_max_confidence": 0.45,
|
|
36
|
+
"council_recommendation": "redesign",
|
|
37
|
+
"must_flag": ["key rotation", "HS256"],
|
|
38
|
+
"must_cite": ["auth-022"],
|
|
39
|
+
"risk_level": "critical",
|
|
40
|
+
"preflight_expects": {
|
|
41
|
+
"touches_sensitive_area": true,
|
|
42
|
+
"sensitive_areas_include": ["auth"],
|
|
43
|
+
"chronicle_conflicts": ["auth-022"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "auth_rs256_valid",
|
|
3
|
+
"description": "Proposing the already-approved RS256 pattern — should proceed",
|
|
4
|
+
"outcome": "Add JWT authentication to the API",
|
|
5
|
+
"design": "RS256 tokens with 15-minute expiry and refresh rotation stored in httpOnly cookies, matching the approved pattern in Chronicle",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "auth-031",
|
|
9
|
+
"key_insight": "RS256 with short-lived tokens and refresh rotation in httpOnly cookies is the approved pattern",
|
|
10
|
+
"decision": "RS256 with 15-min access tokens and refresh rotation stored in httpOnly cookies is the approved auth pattern",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["src/auth/", "src/middleware/"],
|
|
13
|
+
"scope": ["auth", "sessions"],
|
|
14
|
+
"status": "validated",
|
|
15
|
+
"confidence": 0.88,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-03-15T11:00:00Z"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"expected": {
|
|
22
|
+
"jury_min_confidence": 0.65,
|
|
23
|
+
"council_recommendation": "proceed",
|
|
24
|
+
"must_not_flag": ["key rotation problem"],
|
|
25
|
+
"risk_level": "critical",
|
|
26
|
+
"preflight_expects": {
|
|
27
|
+
"touches_sensitive_area": true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "cache_missing_lock",
|
|
3
|
+
"description": "Cache implementation missing stampede protection — should warn or block",
|
|
4
|
+
"outcome": "Cache expensive product catalogue queries in Redis",
|
|
5
|
+
"design": "On cache miss, fetch from database and write to Redis with a 5-minute TTL. No locking strategy.",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "cache-008",
|
|
9
|
+
"key_insight": "Redis cache without stampede protection caused DB overload during peak traffic",
|
|
10
|
+
"decision": "Redis cache without stampede protection caused DB overload — all cache misses hit DB simultaneously during spikes",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["src/cache/", "src/api/products.ts"],
|
|
13
|
+
"scope": ["cache", "performance"],
|
|
14
|
+
"status": "refuted",
|
|
15
|
+
"confidence": 0.85,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-02-20T14:00:00Z"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"expected": {
|
|
22
|
+
"jury_max_confidence": 0.60,
|
|
23
|
+
"council_recommendation": "redesign",
|
|
24
|
+
"must_flag": ["stampede", "lock"],
|
|
25
|
+
"must_cite": ["cache-008"],
|
|
26
|
+
"risk_level": "medium",
|
|
27
|
+
"preflight_expects": {
|
|
28
|
+
"chronicle_conflicts": ["cache-008"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "db_naive_not_null",
|
|
3
|
+
"description": "Naive NOT NULL migration on a large table — should block",
|
|
4
|
+
"outcome": "Add a NOT NULL region column to the users table",
|
|
5
|
+
"design": "Run ALTER TABLE users ADD COLUMN region TEXT NOT NULL DEFAULT 'uk'",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "db-017",
|
|
9
|
+
"key_insight": "Avoid naive locking migrations on large tables",
|
|
10
|
+
"decision": "Avoid naive locking migrations on large tables — use shadow column pattern or pg_repack",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["db/migrations", "src/models/user.ts"],
|
|
13
|
+
"scope": ["database", "migrations"],
|
|
14
|
+
"status": "refuted",
|
|
15
|
+
"confidence": 0.91,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-01-10T10:00:00Z"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"expected": {
|
|
22
|
+
"jury_max_confidence": 0.55,
|
|
23
|
+
"council_recommendation": "redesign",
|
|
24
|
+
"must_flag": ["lock strategy", "rollback"],
|
|
25
|
+
"must_cite": ["db-017"],
|
|
26
|
+
"risk_level": "high",
|
|
27
|
+
"preflight_expects": {
|
|
28
|
+
"rollback_mentioned": false,
|
|
29
|
+
"chronicle_conflicts": ["db-017"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "logging_pii_leak",
|
|
3
|
+
"description": "Logging request bodies that contain PII — should block",
|
|
4
|
+
"outcome": "Add structured request logging to the API for debugging",
|
|
5
|
+
"design": "Log full request body including email, name, and any user-submitted fields to stdout using JSON format",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "sec-003",
|
|
9
|
+
"key_insight": "Logging PII fields violates GDPR and our data retention policy",
|
|
10
|
+
"decision": "Logging PII fields (email, name, address) violates GDPR Article 5 — all request logging must scrub or omit PII fields",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["src/middleware/logger.ts", "src/api/"],
|
|
13
|
+
"scope": ["pii", "compliance", "logging"],
|
|
14
|
+
"status": "validated",
|
|
15
|
+
"confidence": 0.95,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-01-05T08:00:00Z"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"expected": {
|
|
22
|
+
"jury_max_confidence": 0.40,
|
|
23
|
+
"council_recommendation": "redesign",
|
|
24
|
+
"must_flag": ["PII", "GDPR", "email"],
|
|
25
|
+
"must_cite": ["sec-003"],
|
|
26
|
+
"risk_level": "high",
|
|
27
|
+
"preflight_expects": {
|
|
28
|
+
"touches_sensitive_area": true,
|
|
29
|
+
"sensitive_areas_include": ["pii"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "migration_with_rollback",
|
|
3
|
+
"description": "DB migration that explicitly addresses rollback and uses safe pattern — should proceed",
|
|
4
|
+
"outcome": "Add a NOT NULL region column to the users table",
|
|
5
|
+
"design": "Use shadow column pattern: add region TEXT NULLABLE, backfill via batched update, then add NOT NULL constraint after 100% fill confirmed. Rollback: drop shadow column. Uses pg_repack to avoid exclusive locks.",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "db-017",
|
|
9
|
+
"key_insight": "Avoid naive locking migrations on large tables — use shadow column pattern or pg_repack",
|
|
10
|
+
"decision": "Avoid naive locking migrations on large tables — use shadow column pattern or pg_repack",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["db/migrations", "src/models/user.ts"],
|
|
13
|
+
"scope": ["database", "migrations"],
|
|
14
|
+
"status": "refuted",
|
|
15
|
+
"confidence": 0.91,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-01-10T10:00:00Z"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "db-019",
|
|
22
|
+
"key_insight": "Shadow column pattern with batched backfill is the approved approach for NOT NULL migrations",
|
|
23
|
+
"decision": "Shadow column pattern with batched backfill is the approved approach for large NOT NULL migrations",
|
|
24
|
+
"schema_version": 2,
|
|
25
|
+
"affected_areas": ["db/migrations"],
|
|
26
|
+
"scope": ["database", "migrations"],
|
|
27
|
+
"status": "validated",
|
|
28
|
+
"confidence": 0.87,
|
|
29
|
+
"source_module": "council",
|
|
30
|
+
"evidence_cited": ["db-017"],
|
|
31
|
+
"timestamp": "2025-02-01T12:00:00Z"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"expected": {
|
|
35
|
+
"jury_min_confidence": 0.65,
|
|
36
|
+
"council_recommendation": "proceed",
|
|
37
|
+
"risk_level": "high",
|
|
38
|
+
"preflight_expects": {
|
|
39
|
+
"rollback_mentioned": true,
|
|
40
|
+
"chronicle_conflicts": ["db-017"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "no_evidence_novel_design",
|
|
3
|
+
"description": "Novel design with no Chronicle evidence either way — should investigate-more",
|
|
4
|
+
"outcome": "Implement real-time collaboration features using WebSockets",
|
|
5
|
+
"design": "Use Socket.io for bi-directional communication, Redis pub/sub for multi-instance message fanout, and optimistic UI updates with conflict resolution via last-write-wins",
|
|
6
|
+
"oracle_evidence": [],
|
|
7
|
+
"expected": {
|
|
8
|
+
"jury_max_confidence": 0.65,
|
|
9
|
+
"council_recommendation": "investigate-more",
|
|
10
|
+
"risk_level": "medium",
|
|
11
|
+
"preflight_expects": {
|
|
12
|
+
"touches_sensitive_area": false,
|
|
13
|
+
"chronicle_conflicts": []
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "payment_no_idempotency",
|
|
3
|
+
"description": "Payment charge without idempotency key — should block",
|
|
4
|
+
"outcome": "Implement one-click repurchase for customers",
|
|
5
|
+
"design": "On button click, POST /api/charge with the stored card token and amount. Retry on network failure up to 3 times.",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "pay-004",
|
|
9
|
+
"key_insight": "Payment charges without idempotency keys caused duplicate charges during network retries",
|
|
10
|
+
"decision": "All payment charge requests must include a Stripe idempotency key — retries without idempotency keys caused duplicate charges in production",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["src/payments/", "src/api/checkout.ts"],
|
|
13
|
+
"scope": ["payments", "stripe"],
|
|
14
|
+
"status": "refuted",
|
|
15
|
+
"confidence": 0.97,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-04-01T16:00:00Z"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"expected": {
|
|
22
|
+
"jury_max_confidence": 0.40,
|
|
23
|
+
"council_recommendation": "redesign",
|
|
24
|
+
"must_flag": ["idempotency", "duplicate charge"],
|
|
25
|
+
"must_cite": ["pay-004"],
|
|
26
|
+
"risk_level": "critical",
|
|
27
|
+
"preflight_expects": {
|
|
28
|
+
"touches_sensitive_area": true,
|
|
29
|
+
"sensitive_areas_include": ["payments"],
|
|
30
|
+
"chronicle_conflicts": ["pay-004"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "redis_session_rejected",
|
|
3
|
+
"description": "Proposing Redis sessions when they were already removed — should block",
|
|
4
|
+
"outcome": "Implement user session management",
|
|
5
|
+
"design": "Store session data in Redis with a 30-minute TTL and auto-extend on activity. Use express-session with connect-redis.",
|
|
6
|
+
"oracle_evidence": [
|
|
7
|
+
{
|
|
8
|
+
"id": "auth-015",
|
|
9
|
+
"key_insight": "Redis sessions removed due to memory overhead at scale and operational complexity",
|
|
10
|
+
"decision": "Redis sessions removed — memory overhead at scale was unsustainable and operational complexity (Redis cluster, failover) added too much risk",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"affected_areas": ["src/auth/", "src/middleware/session.ts"],
|
|
13
|
+
"scope": ["auth", "sessions", "infrastructure"],
|
|
14
|
+
"status": "refuted",
|
|
15
|
+
"confidence": 0.89,
|
|
16
|
+
"source_module": "council",
|
|
17
|
+
"evidence_cited": [],
|
|
18
|
+
"timestamp": "2025-02-10T15:00:00Z"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"expected": {
|
|
22
|
+
"jury_max_confidence": 0.50,
|
|
23
|
+
"council_recommendation": "redesign",
|
|
24
|
+
"must_flag": ["memory overhead", "Redis"],
|
|
25
|
+
"must_cite": ["auth-015"],
|
|
26
|
+
"risk_level": "critical",
|
|
27
|
+
"preflight_expects": {
|
|
28
|
+
"touches_sensitive_area": true,
|
|
29
|
+
"chronicle_conflicts": ["auth-015"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "safe_refactor",
|
|
3
|
+
"description": "Low-risk internal refactor with no sensitive areas — should proceed without friction",
|
|
4
|
+
"outcome": "Rename internal helper functions in the reporting module for consistency",
|
|
5
|
+
"design": "Rename generateCsvReport to exportReportAsCsv and generatePdfReport to exportReportAsPdf in src/reports/. Update all callers. No behaviour change.",
|
|
6
|
+
"oracle_evidence": [],
|
|
7
|
+
"expected": {
|
|
8
|
+
"jury_min_confidence": 0.70,
|
|
9
|
+
"council_recommendation": "proceed",
|
|
10
|
+
"risk_level": "low",
|
|
11
|
+
"preflight_expects": {
|
|
12
|
+
"touches_sensitive_area": false,
|
|
13
|
+
"rollback_mentioned": false,
|
|
14
|
+
"chronicle_conflicts": []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|