@balpal4495/quorum 3.0.4 → 3.1.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/dist/advisor/ask.d.ts +13 -0
- package/dist/advisor/ask.d.ts.map +1 -0
- package/dist/advisor/ask.js +67 -0
- package/dist/advisor/ask.js.map +1 -0
- package/dist/advisor/index.d.ts +3 -0
- package/dist/advisor/index.d.ts.map +1 -0
- package/dist/advisor/index.js +2 -0
- package/dist/advisor/index.js.map +1 -0
- package/dist/advisor/prompt.d.ts +5 -0
- package/dist/advisor/prompt.d.ts.map +1 -0
- package/{modules/advisor/prompt.ts → dist/advisor/prompt.js} +22 -26
- package/dist/advisor/prompt.js.map +1 -0
- package/dist/advisor/types.d.ts +23 -0
- package/dist/advisor/types.d.ts.map +1 -0
- package/dist/advisor/types.js +2 -0
- package/dist/advisor/types.js.map +1 -0
- package/dist/compass/behavior.d.ts +4 -0
- package/dist/compass/behavior.d.ts.map +1 -0
- package/dist/compass/behavior.js +138 -0
- package/dist/compass/behavior.js.map +1 -0
- package/dist/compass/create.d.ts +3 -0
- package/dist/compass/create.d.ts.map +1 -0
- package/dist/compass/create.js +289 -0
- package/dist/compass/create.js.map +1 -0
- package/dist/compass/evidence/collect.d.ts +11 -0
- package/dist/compass/evidence/collect.d.ts.map +1 -0
- package/dist/compass/evidence/collect.js +86 -0
- package/dist/compass/evidence/collect.js.map +1 -0
- package/dist/compass/index.d.ts +8 -0
- package/dist/compass/index.d.ts.map +1 -0
- package/dist/compass/index.js +8 -0
- package/dist/compass/index.js.map +1 -0
- package/dist/compass/prompts/index.d.ts +28 -0
- package/dist/compass/prompts/index.d.ts.map +1 -0
- package/{modules/compass/prompts/index.ts → dist/compass/prompts/index.js} +13 -38
- package/dist/compass/prompts/index.js.map +1 -0
- package/dist/compass/prompts/system.d.ts +2 -0
- package/dist/compass/prompts/system.d.ts.map +1 -0
- package/{modules/compass/prompts/system.ts → dist/compass/prompts/system.js} +2 -1
- package/dist/compass/prompts/system.js.map +1 -0
- package/dist/compass/propose.d.ts +15 -0
- package/dist/compass/propose.d.ts.map +1 -0
- package/dist/compass/propose.js +128 -0
- package/dist/compass/propose.js.map +1 -0
- package/dist/compass/schemas.d.ts +1271 -0
- package/dist/compass/schemas.d.ts.map +1 -0
- package/dist/compass/schemas.js +113 -0
- package/dist/compass/schemas.js.map +1 -0
- package/dist/compass/score.d.ts +25 -0
- package/dist/compass/score.d.ts.map +1 -0
- package/dist/compass/score.js +89 -0
- package/dist/compass/score.js.map +1 -0
- package/dist/compass/sources/index.d.ts +9 -0
- package/dist/compass/sources/index.d.ts.map +1 -0
- package/dist/compass/sources/index.js +408 -0
- package/dist/compass/sources/index.js.map +1 -0
- package/dist/compass/types.d.ts +334 -0
- package/dist/compass/types.d.ts.map +1 -0
- package/dist/compass/types.js +2 -0
- package/dist/compass/types.js.map +1 -0
- package/dist/council/advisors.d.ts +15 -0
- package/dist/council/advisors.d.ts.map +1 -0
- package/dist/council/advisors.js +46 -0
- package/dist/council/advisors.js.map +1 -0
- package/dist/council/chairman.d.ts +13 -0
- package/dist/council/chairman.d.ts.map +1 -0
- package/dist/council/chairman.js +145 -0
- package/dist/council/chairman.js.map +1 -0
- package/dist/council/deliberate.d.ts +22 -0
- package/dist/council/deliberate.d.ts.map +1 -0
- package/dist/council/deliberate.js +99 -0
- package/dist/council/deliberate.js.map +1 -0
- package/dist/council/frame.d.ts +8 -0
- package/dist/council/frame.d.ts.map +1 -0
- package/dist/council/frame.js +40 -0
- package/dist/council/frame.js.map +1 -0
- package/dist/council/index.d.ts +6 -0
- package/dist/council/index.d.ts.map +1 -0
- package/dist/council/index.js +4 -0
- package/dist/council/index.js.map +1 -0
- package/dist/council/personas.d.ts +18 -0
- package/dist/council/personas.d.ts.map +1 -0
- package/dist/council/personas.js +44 -0
- package/dist/council/personas.js.map +1 -0
- package/dist/council/reviewers.d.ts +13 -0
- package/dist/council/reviewers.d.ts.map +1 -0
- package/dist/council/reviewers.js +59 -0
- package/dist/council/reviewers.js.map +1 -0
- package/dist/council/risk.d.ts +16 -0
- package/dist/council/risk.d.ts.map +1 -0
- package/dist/council/risk.js +74 -0
- package/dist/council/risk.js.map +1 -0
- package/dist/council/types.d.ts +95 -0
- package/dist/council/types.d.ts.map +1 -0
- package/dist/council/types.js +2 -0
- package/dist/council/types.js.map +1 -0
- package/dist/jury/evaluate.d.ts +13 -0
- package/dist/jury/evaluate.d.ts.map +1 -0
- package/{modules/jury/evaluate.ts → dist/jury/evaluate.js} +60 -84
- package/dist/jury/evaluate.js.map +1 -0
- package/dist/jury/index.d.ts +6 -0
- package/dist/jury/index.d.ts.map +1 -0
- package/dist/jury/index.js +4 -0
- package/dist/jury/index.js.map +1 -0
- package/dist/jury/preflight.d.ts +26 -0
- package/dist/jury/preflight.d.ts.map +1 -0
- package/dist/jury/preflight.js +71 -0
- package/dist/jury/preflight.js.map +1 -0
- package/dist/jury/schema.d.ts +57 -0
- package/dist/jury/schema.d.ts.map +1 -0
- package/dist/jury/schema.js +21 -0
- package/dist/jury/schema.js.map +1 -0
- package/dist/jury/types.d.ts +47 -0
- package/dist/jury/types.d.ts.map +1 -0
- package/dist/jury/types.js +2 -0
- package/dist/jury/types.js.map +1 -0
- package/dist/oracle/adapters/lance-db.d.ts +15 -0
- package/dist/oracle/adapters/lance-db.d.ts.map +1 -0
- package/dist/oracle/adapters/lance-db.js +68 -0
- package/dist/oracle/adapters/lance-db.js.map +1 -0
- package/dist/oracle/adapters/xenova-embedder.d.ts +21 -0
- package/dist/oracle/adapters/xenova-embedder.d.ts.map +1 -0
- package/dist/oracle/adapters/xenova-embedder.js +36 -0
- package/dist/oracle/adapters/xenova-embedder.js.map +1 -0
- package/dist/oracle/bm25.d.ts +20 -0
- package/dist/oracle/bm25.d.ts.map +1 -0
- package/dist/oracle/bm25.js +82 -0
- package/dist/oracle/bm25.js.map +1 -0
- package/dist/oracle/index.d.ts +21 -0
- package/dist/oracle/index.d.ts.map +1 -0
- package/dist/oracle/index.js +25 -0
- package/dist/oracle/index.js.map +1 -0
- package/dist/oracle/log.d.ts +6 -0
- package/dist/oracle/log.d.ts.map +1 -0
- package/dist/oracle/log.js +12 -0
- package/dist/oracle/log.js.map +1 -0
- package/dist/oracle/propose.d.ts +25 -0
- package/dist/oracle/propose.d.ts.map +1 -0
- package/dist/oracle/propose.js +133 -0
- package/dist/oracle/propose.js.map +1 -0
- package/dist/oracle/query.d.ts +17 -0
- package/dist/oracle/query.d.ts.map +1 -0
- package/dist/oracle/query.js +106 -0
- package/dist/oracle/query.js.map +1 -0
- package/dist/oracle/summary.d.ts +11 -0
- package/dist/oracle/summary.d.ts.map +1 -0
- package/dist/oracle/summary.js +102 -0
- package/dist/oracle/summary.js.map +1 -0
- package/dist/oracle/types.d.ts +31 -0
- package/dist/oracle/types.d.ts.map +1 -0
- package/dist/oracle/types.js +2 -0
- package/dist/oracle/types.js.map +1 -0
- package/dist/sentinel/assert.d.ts +28 -0
- package/dist/sentinel/assert.d.ts.map +1 -0
- package/dist/sentinel/assert.js +63 -0
- package/dist/sentinel/assert.js.map +1 -0
- package/dist/sentinel/coverage.d.ts +14 -0
- package/dist/sentinel/coverage.d.ts.map +1 -0
- package/dist/sentinel/coverage.js +96 -0
- package/dist/sentinel/coverage.js.map +1 -0
- package/dist/sentinel/drift.d.ts +12 -0
- package/dist/sentinel/drift.d.ts.map +1 -0
- package/dist/sentinel/drift.js +149 -0
- package/dist/sentinel/drift.js.map +1 -0
- package/dist/sentinel/index.d.ts +7 -0
- package/dist/sentinel/index.d.ts.map +1 -0
- package/dist/sentinel/index.js +5 -0
- package/dist/sentinel/index.js.map +1 -0
- package/dist/sentinel/review.d.ts +15 -0
- package/dist/sentinel/review.d.ts.map +1 -0
- package/dist/sentinel/review.js +177 -0
- package/dist/sentinel/review.js.map +1 -0
- package/dist/setup.d.ts +103 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +87 -0
- package/dist/setup.js.map +1 -0
- package/dist/shared/types.d.ts +173 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +16 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +13 -8
- package/.github/copilot-instructions.md +0 -117
- package/CLAUDE.md +0 -146
- package/GEMINI.md +0 -73
- package/SETUP.md +0 -264
- package/evals/__tests__/eval.test.ts +0 -31
- package/evals/cases/auth_hs256_rejected.json +0 -46
- package/evals/cases/auth_rs256_valid.json +0 -30
- package/evals/cases/cache_missing_lock.json +0 -31
- package/evals/cases/db_naive_not_null.json +0 -32
- package/evals/cases/logging_pii_leak.json +0 -32
- package/evals/cases/migration_with_rollback.json +0 -43
- package/evals/cases/no_evidence_novel_design.json +0 -16
- package/evals/cases/payment_no_idempotency.json +0 -33
- package/evals/cases/redis_session_rejected.json +0 -32
- package/evals/cases/safe_refactor.json +0 -17
- package/evals/runner.ts +0 -226
- package/modules/AGENTS.md +0 -78
- package/modules/CLAUDE.md +0 -93
- package/modules/README.md +0 -504
- package/modules/advisor/ask.ts +0 -87
- package/modules/advisor/index.ts +0 -2
- package/modules/advisor/types.ts +0 -26
- package/modules/compass/behavior.ts +0 -161
- package/modules/compass/create.ts +0 -365
- package/modules/compass/evidence/collect.ts +0 -109
- package/modules/compass/index.ts +0 -7
- package/modules/compass/propose.ts +0 -152
- package/modules/compass/schemas.ts +0 -121
- package/modules/compass/score.ts +0 -77
- package/modules/compass/sources/index.ts +0 -413
- package/modules/compass/types.ts +0 -431
- package/modules/council/advisors.ts +0 -71
- package/modules/council/chairman.ts +0 -183
- package/modules/council/deliberate.ts +0 -141
- package/modules/council/frame.ts +0 -54
- package/modules/council/index.ts +0 -9
- package/modules/council/personas.ts +0 -57
- package/modules/council/reviewers.ts +0 -82
- package/modules/council/risk.ts +0 -89
- package/modules/council/types.ts +0 -107
- package/modules/jury/index.ts +0 -5
- package/modules/jury/preflight.ts +0 -101
- package/modules/jury/schema.ts +0 -24
- package/modules/jury/types.ts +0 -50
- package/modules/oracle/adapters/lance-db.ts +0 -81
- package/modules/oracle/adapters/xenova-embedder.ts +0 -43
- package/modules/oracle/bm25.ts +0 -92
- package/modules/oracle/index.ts +0 -36
- package/modules/oracle/log.ts +0 -15
- package/modules/oracle/propose.ts +0 -164
- package/modules/oracle/query.ts +0 -146
- package/modules/oracle/summary.ts +0 -116
- package/modules/oracle/types.ts +0 -32
- package/modules/sentinel/assert.ts +0 -95
- package/modules/sentinel/coverage.ts +0 -106
- package/modules/sentinel/drift.ts +0 -163
- package/modules/sentinel/index.ts +0 -6
- package/modules/sentinel/review.ts +0 -208
- package/modules/setup.ts +0 -202
- package/modules/shared/types.ts +0 -193
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import type { ChronicleEntry } from "../shared/types"
|
|
4
|
-
import { entryText } from "../shared/types"
|
|
5
|
-
|
|
6
|
-
const SUMMARY_WEEKS = 12
|
|
7
|
-
const DIRECTIVE =
|
|
8
|
-
"<!-- Chronicle Summary v1 — temporal orientation for agents. " +
|
|
9
|
-
"Use for sequence context; query Oracle by entry ID for full reasoning. -->"
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Returns the ISO week string (YYYY-Www) for a given date.
|
|
13
|
-
* Uses the ISO 8601 definition: week 1 is the week containing the first Thursday.
|
|
14
|
-
*/
|
|
15
|
-
function isoWeekKey(date: Date): string {
|
|
16
|
-
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
|
|
17
|
-
const day = d.getUTCDay() || 7
|
|
18
|
-
d.setUTCDate(d.getUTCDate() + 4 - day)
|
|
19
|
-
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
|
20
|
-
const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7)
|
|
21
|
-
return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function workRefLabel(entry: ChronicleEntry): string {
|
|
25
|
-
if (!entry.work_ref) return "__none__"
|
|
26
|
-
const { type, ref } = entry.work_ref
|
|
27
|
-
return ref ? `[${type} ${ref}]` : `[${type}]`
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function renderEntry(entry: ChronicleEntry): string {
|
|
31
|
-
const areas = entry.affected_areas.join(", ")
|
|
32
|
-
const id = entry.id.slice(0, 8)
|
|
33
|
-
return `- **[${id}]** ${areas} — \`${entry.status}\` (${entry.confidence.toFixed(2)}) — ${entryText(entry)}`
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Rebuild .chronicle/SUMMARY.md from all committed entries.
|
|
38
|
-
*
|
|
39
|
-
* Groups entries by ISO week (most-recent first), then by work_ref within
|
|
40
|
-
* each week. Shows the last SUMMARY_WEEKS weeks; older entries are omitted
|
|
41
|
-
* (still fully queryable via Oracle).
|
|
42
|
-
*
|
|
43
|
-
* Called by commit() as a best-effort side-effect — never throws.
|
|
44
|
-
*/
|
|
45
|
-
export async function updateSummary(chronicleDir: string): Promise<void> {
|
|
46
|
-
const committedDir = path.join(chronicleDir, "committed")
|
|
47
|
-
|
|
48
|
-
let files: string[]
|
|
49
|
-
try {
|
|
50
|
-
files = await fs.readdir(committedDir)
|
|
51
|
-
} catch {
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const entries: ChronicleEntry[] = []
|
|
56
|
-
for (const file of files) {
|
|
57
|
-
if (!file.endsWith(".json")) continue
|
|
58
|
-
try {
|
|
59
|
-
const raw = await fs.readFile(path.join(committedDir, file), "utf8")
|
|
60
|
-
entries.push(JSON.parse(raw) as ChronicleEntry)
|
|
61
|
-
} catch {
|
|
62
|
-
// Skip malformed entries
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (entries.length === 0) return
|
|
67
|
-
|
|
68
|
-
entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
69
|
-
|
|
70
|
-
// Group by ISO week
|
|
71
|
-
const byWeek = new Map<string, ChronicleEntry[]>()
|
|
72
|
-
for (const entry of entries) {
|
|
73
|
-
const week = isoWeekKey(new Date(entry.timestamp))
|
|
74
|
-
const bucket = byWeek.get(week) ?? []
|
|
75
|
-
bucket.push(entry)
|
|
76
|
-
byWeek.set(week, bucket)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const weeks = [...byWeek.keys()].sort().reverse().slice(0, SUMMARY_WEEKS)
|
|
80
|
-
|
|
81
|
-
const lines: string[] = [DIRECTIVE, ""]
|
|
82
|
-
|
|
83
|
-
for (const week of weeks) {
|
|
84
|
-
lines.push(`## Week ${week}`, "")
|
|
85
|
-
|
|
86
|
-
// Group entries within week by work_ref label
|
|
87
|
-
const weekEntries = byWeek.get(week)!
|
|
88
|
-
const byWork = new Map<string, ChronicleEntry[]>()
|
|
89
|
-
for (const entry of weekEntries) {
|
|
90
|
-
const key = workRefLabel(entry)
|
|
91
|
-
const bucket = byWork.get(key) ?? []
|
|
92
|
-
bucket.push(entry)
|
|
93
|
-
byWork.set(key, bucket)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Labelled work groups first, then ungrouped
|
|
97
|
-
const workKeys = [...byWork.keys()].sort((a, b) =>
|
|
98
|
-
a === "__none__" ? 1 : b === "__none__" ? -1 : a.localeCompare(b),
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
for (const key of workKeys) {
|
|
102
|
-
if (key === "__none__") {
|
|
103
|
-
lines.push(`### (no work context — query Oracle by entry ID for details)`)
|
|
104
|
-
} else {
|
|
105
|
-
lines.push(`### ${key}`)
|
|
106
|
-
}
|
|
107
|
-
for (const entry of byWork.get(key)!) {
|
|
108
|
-
lines.push(renderEntry(entry))
|
|
109
|
-
}
|
|
110
|
-
lines.push("")
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const summaryPath = path.join(chronicleDir, "SUMMARY.md")
|
|
115
|
-
await fs.writeFile(summaryPath, lines.join("\n"), "utf8")
|
|
116
|
-
}
|
package/modules/oracle/types.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { ChronicleEntry } from "../shared/types"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Abstract vector store interface.
|
|
5
|
-
* Swap implementations without changing Oracle logic.
|
|
6
|
-
* Default implementation: LanceDB (see adapters/lance-db.ts).
|
|
7
|
-
*/
|
|
8
|
-
export interface VectorStore {
|
|
9
|
-
/**
|
|
10
|
-
* Upsert a Chronicle entry with its embedding vector.
|
|
11
|
-
* If an entry with this ID already exists, it is replaced.
|
|
12
|
-
*/
|
|
13
|
-
upsert: (id: string, vector: number[], metadata: ChronicleEntry) => Promise<void>
|
|
14
|
-
/**
|
|
15
|
-
* Return the top-K most similar entries to the given query vector.
|
|
16
|
-
* Scores should be in [0, 1] (higher = more similar).
|
|
17
|
-
*/
|
|
18
|
-
search: (
|
|
19
|
-
vector: number[],
|
|
20
|
-
limit: number,
|
|
21
|
-
) => Promise<Array<{ entry: ChronicleEntry; score: number }>>
|
|
22
|
-
/** Return all stored entries (used for full-corpus BM25 if needed). */
|
|
23
|
-
getAll: () => Promise<ChronicleEntry[]>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface OracleDeps {
|
|
27
|
-
/** Converts text to a numeric embedding vector. */
|
|
28
|
-
embedder: (text: string) => Promise<number[]>
|
|
29
|
-
vectorStore: VectorStore
|
|
30
|
-
/** Root directory for Chronicle data. Default: ".chronicle" */
|
|
31
|
-
chronicleDir?: string
|
|
32
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { coverage } from "./coverage"
|
|
2
|
-
import { detectDrift } from "./drift"
|
|
3
|
-
import type { LLMProvider } from "../shared/types"
|
|
4
|
-
|
|
5
|
-
export interface SentinelAssertOptions {
|
|
6
|
-
chronicleDir?: string
|
|
7
|
-
codebasePath?: string
|
|
8
|
-
/** When provided, drift detection runs. When absent, drift tests are skipped. */
|
|
9
|
-
llm?: LLMProvider
|
|
10
|
-
extensions?: string[]
|
|
11
|
-
/**
|
|
12
|
-
* Chronicle coverage must reach this percentage for the CI test to pass.
|
|
13
|
-
* Default 0 = report gaps as advisory output without failing the build.
|
|
14
|
-
* Raise this as the project matures (e.g. 50 for an established codebase).
|
|
15
|
-
*/
|
|
16
|
-
minCoveragePercent?: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Returns a set of named assertions designed to be called inside a Vitest
|
|
21
|
-
* describe block. Coverage assertions are deterministic and always run.
|
|
22
|
-
* Drift assertions skip gracefully when no LLM is provided.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* import { describe } from "vitest"
|
|
26
|
-
* import { sentinelAssertions } from "../modules/sentinel/assert"
|
|
27
|
-
*
|
|
28
|
-
* const assertions = sentinelAssertions({ chronicleDir: ".chronicle", codebasePath: "modules" })
|
|
29
|
-
* describe("sentinel", () => { assertions.forEach(a => a()) })
|
|
30
|
-
*/
|
|
31
|
-
export function sentinelAssertions(options: SentinelAssertOptions = {}): Array<() => void> {
|
|
32
|
-
const {
|
|
33
|
-
chronicleDir = ".chronicle",
|
|
34
|
-
codebasePath = ".",
|
|
35
|
-
llm,
|
|
36
|
-
extensions,
|
|
37
|
-
minCoveragePercent = 0,
|
|
38
|
-
} = options
|
|
39
|
-
|
|
40
|
-
// Import vitest lazily so this file is usable outside of a test context too
|
|
41
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
42
|
-
const { it, expect, describe: _describe } = require("vitest") as typeof import("vitest")
|
|
43
|
-
|
|
44
|
-
const assertions: Array<() => void> = []
|
|
45
|
-
|
|
46
|
-
// ── Coverage (deterministic, always run) ──────────────────────────────────
|
|
47
|
-
assertions.push(() => {
|
|
48
|
-
const label = minCoveragePercent > 0
|
|
49
|
-
? `coverage: Chronicle coverage ≥ ${minCoveragePercent}%`
|
|
50
|
-
: "coverage: Chronicle coverage report [advisory]"
|
|
51
|
-
it(label, async () => {
|
|
52
|
-
const report = await coverage(chronicleDir, codebasePath, { extensions })
|
|
53
|
-
if (report.uncoveredFiles.length > 0) {
|
|
54
|
-
const list = report.uncoveredFiles.slice(0, 10).join("\n ")
|
|
55
|
-
const msg = `${report.uncoveredFiles.length} source file(s) have no Chronicle coverage (${report.percentage}% covered):\n ${list}`
|
|
56
|
-
if (minCoveragePercent > 0) {
|
|
57
|
-
expect(report.percentage, msg).toBeGreaterThanOrEqual(minCoveragePercent)
|
|
58
|
-
} else {
|
|
59
|
-
// New project or no threshold set — surface gaps without failing the build
|
|
60
|
-
console.info(`[sentinel] ${msg}`)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
assertions.push(() => {
|
|
67
|
-
it("coverage: report is readable and well-formed", async () => {
|
|
68
|
-
const report = await coverage(chronicleDir, codebasePath, { extensions })
|
|
69
|
-
expect(report.totalFiles).toBeGreaterThanOrEqual(0)
|
|
70
|
-
expect(report.percentage).toBeGreaterThanOrEqual(0)
|
|
71
|
-
expect(report.percentage).toBeLessThanOrEqual(100)
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// ── Drift (advisory, skips when no LLM configured) ────────────────────────
|
|
76
|
-
assertions.push(() => {
|
|
77
|
-
it.skipIf(!llm)(
|
|
78
|
-
"drift: no Chronicle entries flagged as potentially stale [advisory]",
|
|
79
|
-
async () => {
|
|
80
|
-
const report = await detectDrift(chronicleDir, codebasePath, llm!)
|
|
81
|
-
if (report.flags.length > 0) {
|
|
82
|
-
const detail = report.flags
|
|
83
|
-
.map(f => ` [${f.entryId.slice(0, 8)}] ${f.keyInsight}\n → ${f.reasoning}`)
|
|
84
|
-
.join("\n")
|
|
85
|
-
expect(
|
|
86
|
-
report.flags,
|
|
87
|
-
`${report.flags.length} Chronicle entry/entries may have drifted (advisory — review before marking refuted):\n${detail}`,
|
|
88
|
-
).toHaveLength(0)
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
return assertions
|
|
95
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { promises as fs, Dirent } from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import type { ChronicleEntry, CoverageReport, FileCoverage } from "../shared/types"
|
|
4
|
-
|
|
5
|
-
const IGNORED_DIRS = new Set(["node_modules", "dist", ".git", ".chronicle", "coverage", "__tests__"])
|
|
6
|
-
const TEST_SUFFIXES = [".test.ts", ".spec.ts", ".test.js", ".spec.js"]
|
|
7
|
-
|
|
8
|
-
async function walkFiles(
|
|
9
|
-
dir: string,
|
|
10
|
-
extensions: string[],
|
|
11
|
-
excludeTestFiles: boolean,
|
|
12
|
-
): Promise<string[]> {
|
|
13
|
-
const results: string[] = []
|
|
14
|
-
|
|
15
|
-
async function recurse(current: string): Promise<void> {
|
|
16
|
-
let entries: Dirent<string>[]
|
|
17
|
-
try {
|
|
18
|
-
entries = await fs.readdir(current, { withFileTypes: true, encoding: "utf8" })
|
|
19
|
-
} catch {
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
if (entry.isDirectory()) {
|
|
24
|
-
if (!IGNORED_DIRS.has(entry.name)) await recurse(path.join(current, entry.name))
|
|
25
|
-
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
26
|
-
if (excludeTestFiles && TEST_SUFFIXES.some(s => entry.name.endsWith(s))) continue
|
|
27
|
-
results.push(path.join(current, entry.name))
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
await recurse(dir)
|
|
33
|
-
return results
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function readCommittedEntries(chronicleDir: string): Promise<ChronicleEntry[]> {
|
|
37
|
-
const committedDir = path.join(chronicleDir, "committed")
|
|
38
|
-
let files: string[]
|
|
39
|
-
try {
|
|
40
|
-
files = await fs.readdir(committedDir)
|
|
41
|
-
} catch {
|
|
42
|
-
return []
|
|
43
|
-
}
|
|
44
|
-
const entries: ChronicleEntry[] = []
|
|
45
|
-
for (const file of files) {
|
|
46
|
-
if (!file.endsWith(".json")) continue
|
|
47
|
-
try {
|
|
48
|
-
const raw = await fs.readFile(path.join(committedDir, file), "utf8")
|
|
49
|
-
entries.push(JSON.parse(raw) as ChronicleEntry)
|
|
50
|
-
} catch {
|
|
51
|
-
// skip malformed
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return entries
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isCovered(relativePath: string, entries: ChronicleEntry[]): { covered: boolean; entryIds: string[] } {
|
|
58
|
-
const matched: string[] = []
|
|
59
|
-
const normalised = relativePath.replace(/\\/g, "/")
|
|
60
|
-
for (const entry of entries) {
|
|
61
|
-
const hits = entry.affected_areas.some(area => {
|
|
62
|
-
const normArea = area.replace(/\\/g, "/")
|
|
63
|
-
return normalised.includes(normArea) || normArea.includes(normalised)
|
|
64
|
-
})
|
|
65
|
-
if (hits) matched.push(entry.id)
|
|
66
|
-
}
|
|
67
|
-
return { covered: matched.length > 0, entryIds: matched }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Scan the codebase and report which files have Chronicle entries referencing
|
|
72
|
-
* them in affected_areas and which do not.
|
|
73
|
-
*
|
|
74
|
-
* Matching is substring-based — "oracle/propose.ts" in affected_areas covers
|
|
75
|
-
* "modules/oracle/propose.ts" in the codebase. Treat percentage as directional
|
|
76
|
-
* signal, not a precision metric.
|
|
77
|
-
*/
|
|
78
|
-
export async function coverage(
|
|
79
|
-
chronicleDir: string,
|
|
80
|
-
codebasePath: string,
|
|
81
|
-
options: { extensions?: string[]; excludeTestFiles?: boolean } = {},
|
|
82
|
-
): Promise<CoverageReport> {
|
|
83
|
-
const extensions = options.extensions ?? [".ts"]
|
|
84
|
-
const excludeTestFiles = options.excludeTestFiles ?? true
|
|
85
|
-
const [entries, files] = await Promise.all([
|
|
86
|
-
readCommittedEntries(chronicleDir),
|
|
87
|
-
walkFiles(codebasePath, extensions, excludeTestFiles),
|
|
88
|
-
])
|
|
89
|
-
|
|
90
|
-
const coverageByFile: FileCoverage[] = files.map(absolute => {
|
|
91
|
-
const relative = path.relative(codebasePath, absolute).replace(/\\/g, "/")
|
|
92
|
-
const { covered, entryIds } = isCovered(relative, entries)
|
|
93
|
-
return { file: relative, covered, entryIds }
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
const covered = coverageByFile.filter(f => f.covered)
|
|
97
|
-
const uncovered = coverageByFile.filter(f => !f.covered)
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
totalFiles: files.length,
|
|
101
|
-
coveredFiles: covered.length,
|
|
102
|
-
uncoveredFiles: uncovered.map(f => f.file),
|
|
103
|
-
coverageByFile,
|
|
104
|
-
percentage: files.length === 0 ? 0 : Math.round((covered.length / files.length) * 100),
|
|
105
|
-
}
|
|
106
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import type { ChronicleEntry, DriftFlag, DriftReport, LLMProvider } from "../shared/types"
|
|
4
|
-
import { entryText } from "../shared/types"
|
|
5
|
-
|
|
6
|
-
const FILE_CONTENT_LIMIT = 3000
|
|
7
|
-
|
|
8
|
-
async function readCommittedEntries(chronicleDir: string): Promise<ChronicleEntry[]> {
|
|
9
|
-
const committedDir = path.join(chronicleDir, "committed")
|
|
10
|
-
let files: string[]
|
|
11
|
-
try {
|
|
12
|
-
files = await fs.readdir(committedDir)
|
|
13
|
-
} catch {
|
|
14
|
-
return []
|
|
15
|
-
}
|
|
16
|
-
const entries: ChronicleEntry[] = []
|
|
17
|
-
for (const file of files) {
|
|
18
|
-
if (!file.endsWith(".json")) continue
|
|
19
|
-
try {
|
|
20
|
-
const raw = await fs.readFile(path.join(committedDir, file), "utf8")
|
|
21
|
-
entries.push(JSON.parse(raw) as ChronicleEntry)
|
|
22
|
-
} catch {
|
|
23
|
-
// skip malformed
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return entries
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function resolveLocalFiles(areas: string[], codebasePath: string): Promise<string[]> {
|
|
30
|
-
const resolved: string[] = []
|
|
31
|
-
for (const area of areas) {
|
|
32
|
-
// Try as a direct relative path first
|
|
33
|
-
const candidate = path.join(codebasePath, area)
|
|
34
|
-
try {
|
|
35
|
-
await fs.access(candidate)
|
|
36
|
-
resolved.push(candidate)
|
|
37
|
-
continue
|
|
38
|
-
} catch {
|
|
39
|
-
// not a direct path — try substring search
|
|
40
|
-
}
|
|
41
|
-
// Walk up to two levels to find files whose relative path contains the area string
|
|
42
|
-
try {
|
|
43
|
-
const all = await fs.readdir(codebasePath, { recursive: true, encoding: "utf8" })
|
|
44
|
-
for (const f of all) {
|
|
45
|
-
const normalised = f.replace(/\\/g, "/")
|
|
46
|
-
if (normalised.includes(area.replace(/\\/g, "/")) && normalised.endsWith(".ts")) {
|
|
47
|
-
resolved.push(path.join(codebasePath, f))
|
|
48
|
-
break
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
// ignore
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return [...new Set(resolved)]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function evaluateDrift(
|
|
59
|
-
entry: ChronicleEntry,
|
|
60
|
-
files: Array<{ filePath: string; content: string }>,
|
|
61
|
-
llm: LLMProvider,
|
|
62
|
-
): Promise<DriftFlag> {
|
|
63
|
-
const fileSection = files
|
|
64
|
-
.map(f => `### ${path.basename(f.filePath)}\n\`\`\`\n${f.content.slice(0, FILE_CONTENT_LIMIT)}\n\`\`\``)
|
|
65
|
-
.join("\n\n")
|
|
66
|
-
|
|
67
|
-
const response = await llm([
|
|
68
|
-
{
|
|
69
|
-
role: "system",
|
|
70
|
-
content:
|
|
71
|
-
"You are a code reviewer checking whether a documented insight still accurately describes the current source code. " +
|
|
72
|
-
"Reply with a JSON object only — no markdown, no explanation outside the object.",
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
role: "user",
|
|
76
|
-
content:
|
|
77
|
-
`Documented insight:
|
|
78
|
-
"${entryText(entry)}"
|
|
79
|
-
|
|
80
|
-
` +
|
|
81
|
-
`Current source:\n${fileSection}\n\n` +
|
|
82
|
-
`Does this insight still accurately describe the code above?\n` +
|
|
83
|
-
`{"stillValid": boolean, "confidence": number, "reasoning": "one sentence"}`,
|
|
84
|
-
},
|
|
85
|
-
])
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const match = response.match(/\{[\s\S]*?\}/)
|
|
89
|
-
if (!match) throw new Error("no JSON")
|
|
90
|
-
const parsed = JSON.parse(match[0]) as { stillValid?: unknown; confidence?: unknown; reasoning?: unknown }
|
|
91
|
-
return {
|
|
92
|
-
entryId: entry.id,
|
|
93
|
-
keyInsight: entryText(entry),
|
|
94
|
-
affectedFiles: files.map(f => f.filePath),
|
|
95
|
-
stillValid: Boolean(parsed.stillValid),
|
|
96
|
-
confidence: typeof parsed.confidence === "number" ? Math.max(0, Math.min(1, parsed.confidence)) : 0.5,
|
|
97
|
-
reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : "no reasoning provided",
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
// Parse failure → conservative: flag for human review
|
|
101
|
-
return {
|
|
102
|
-
entryId: entry.id,
|
|
103
|
-
keyInsight: entryText(entry),
|
|
104
|
-
affectedFiles: files.map(f => f.filePath),
|
|
105
|
-
stillValid: false,
|
|
106
|
-
confidence: 0,
|
|
107
|
-
reasoning: "LLM response could not be parsed — manual review recommended",
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* For each Chronicle entry whose affected_areas resolves to at least one local
|
|
114
|
-
* source file, ask the LLM whether the key_insight still accurately describes
|
|
115
|
-
* the current code.
|
|
116
|
-
*
|
|
117
|
-
* Output is strictly advisory — entries are never updated autonomously.
|
|
118
|
-
* Entries where no affected_areas value resolves to a local file are skipped
|
|
119
|
-
* (e.g. entries about external tools, workflows, or conceptual areas).
|
|
120
|
-
*/
|
|
121
|
-
export async function detectDrift(
|
|
122
|
-
chronicleDir: string,
|
|
123
|
-
codebasePath: string,
|
|
124
|
-
llm: LLMProvider,
|
|
125
|
-
): Promise<DriftReport> {
|
|
126
|
-
const entries = await readCommittedEntries(chronicleDir)
|
|
127
|
-
|
|
128
|
-
const flags: DriftFlag[] = []
|
|
129
|
-
const confirmed: DriftFlag[] = []
|
|
130
|
-
const skipped: string[] = []
|
|
131
|
-
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
const localPaths = await resolveLocalFiles(entry.affected_areas, codebasePath)
|
|
134
|
-
if (localPaths.length === 0) {
|
|
135
|
-
skipped.push(entry.id)
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const files: Array<{ filePath: string; content: string }> = []
|
|
140
|
-
for (const p of localPaths) {
|
|
141
|
-
try {
|
|
142
|
-
const content = await fs.readFile(p, "utf8")
|
|
143
|
-
files.push({ filePath: p, content })
|
|
144
|
-
} catch {
|
|
145
|
-
// file unreadable — skip this path
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (files.length === 0) {
|
|
150
|
-
skipped.push(entry.id)
|
|
151
|
-
continue
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const result = await evaluateDrift(entry, files, llm)
|
|
155
|
-
if (result.stillValid) {
|
|
156
|
-
confirmed.push(result)
|
|
157
|
-
} else {
|
|
158
|
-
flags.push(result)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return { checkedAt: new Date().toISOString(), flags, confirmed, skipped }
|
|
163
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { coverage } from "./coverage"
|
|
2
|
-
export { detectDrift } from "./drift"
|
|
3
|
-
export { reviewContext } from "./review"
|
|
4
|
-
export { sentinelAssertions } from "./assert"
|
|
5
|
-
export type { CoverageReport, FileCoverage, DriftReport, DriftFlag } from "../shared/types"
|
|
6
|
-
export type { SentinelAssertOptions } from "./assert"
|