@balpal4495/quorum 3.0.3 → 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.
Files changed (243) hide show
  1. package/bin/commands/compass.js +4 -4
  2. package/bin/shared/llm.js +2 -2
  3. package/dist/advisor/ask.d.ts +13 -0
  4. package/dist/advisor/ask.d.ts.map +1 -0
  5. package/dist/advisor/ask.js +67 -0
  6. package/dist/advisor/ask.js.map +1 -0
  7. package/dist/advisor/index.d.ts +3 -0
  8. package/dist/advisor/index.d.ts.map +1 -0
  9. package/dist/advisor/index.js +2 -0
  10. package/dist/advisor/index.js.map +1 -0
  11. package/dist/advisor/prompt.d.ts +5 -0
  12. package/dist/advisor/prompt.d.ts.map +1 -0
  13. package/{modules/advisor/prompt.ts → dist/advisor/prompt.js} +22 -26
  14. package/dist/advisor/prompt.js.map +1 -0
  15. package/dist/advisor/types.d.ts +23 -0
  16. package/dist/advisor/types.d.ts.map +1 -0
  17. package/dist/advisor/types.js +2 -0
  18. package/dist/advisor/types.js.map +1 -0
  19. package/dist/compass/behavior.d.ts +4 -0
  20. package/dist/compass/behavior.d.ts.map +1 -0
  21. package/dist/compass/behavior.js +138 -0
  22. package/dist/compass/behavior.js.map +1 -0
  23. package/dist/compass/create.d.ts +3 -0
  24. package/dist/compass/create.d.ts.map +1 -0
  25. package/dist/compass/create.js +289 -0
  26. package/dist/compass/create.js.map +1 -0
  27. package/dist/compass/evidence/collect.d.ts +11 -0
  28. package/dist/compass/evidence/collect.d.ts.map +1 -0
  29. package/dist/compass/evidence/collect.js +86 -0
  30. package/dist/compass/evidence/collect.js.map +1 -0
  31. package/dist/compass/index.d.ts +8 -0
  32. package/dist/compass/index.d.ts.map +1 -0
  33. package/dist/compass/index.js +8 -0
  34. package/dist/compass/index.js.map +1 -0
  35. package/dist/compass/prompts/index.d.ts +28 -0
  36. package/dist/compass/prompts/index.d.ts.map +1 -0
  37. package/{modules/compass/prompts/index.ts → dist/compass/prompts/index.js} +13 -38
  38. package/dist/compass/prompts/index.js.map +1 -0
  39. package/dist/compass/prompts/system.d.ts +2 -0
  40. package/dist/compass/prompts/system.d.ts.map +1 -0
  41. package/{modules/compass/prompts/system.ts → dist/compass/prompts/system.js} +2 -1
  42. package/dist/compass/prompts/system.js.map +1 -0
  43. package/dist/compass/propose.d.ts +15 -0
  44. package/dist/compass/propose.d.ts.map +1 -0
  45. package/dist/compass/propose.js +128 -0
  46. package/dist/compass/propose.js.map +1 -0
  47. package/dist/compass/schemas.d.ts +1271 -0
  48. package/dist/compass/schemas.d.ts.map +1 -0
  49. package/dist/compass/schemas.js +113 -0
  50. package/dist/compass/schemas.js.map +1 -0
  51. package/dist/compass/score.d.ts +25 -0
  52. package/dist/compass/score.d.ts.map +1 -0
  53. package/dist/compass/score.js +89 -0
  54. package/dist/compass/score.js.map +1 -0
  55. package/dist/compass/sources/index.d.ts +9 -0
  56. package/dist/compass/sources/index.d.ts.map +1 -0
  57. package/dist/compass/sources/index.js +408 -0
  58. package/dist/compass/sources/index.js.map +1 -0
  59. package/dist/compass/types.d.ts +334 -0
  60. package/dist/compass/types.d.ts.map +1 -0
  61. package/dist/compass/types.js +2 -0
  62. package/dist/compass/types.js.map +1 -0
  63. package/dist/council/advisors.d.ts +15 -0
  64. package/dist/council/advisors.d.ts.map +1 -0
  65. package/dist/council/advisors.js +46 -0
  66. package/dist/council/advisors.js.map +1 -0
  67. package/dist/council/chairman.d.ts +13 -0
  68. package/dist/council/chairman.d.ts.map +1 -0
  69. package/dist/council/chairman.js +145 -0
  70. package/dist/council/chairman.js.map +1 -0
  71. package/dist/council/deliberate.d.ts +22 -0
  72. package/dist/council/deliberate.d.ts.map +1 -0
  73. package/dist/council/deliberate.js +99 -0
  74. package/dist/council/deliberate.js.map +1 -0
  75. package/dist/council/frame.d.ts +8 -0
  76. package/dist/council/frame.d.ts.map +1 -0
  77. package/dist/council/frame.js +40 -0
  78. package/dist/council/frame.js.map +1 -0
  79. package/dist/council/index.d.ts +6 -0
  80. package/dist/council/index.d.ts.map +1 -0
  81. package/dist/council/index.js +4 -0
  82. package/dist/council/index.js.map +1 -0
  83. package/dist/council/personas.d.ts +18 -0
  84. package/dist/council/personas.d.ts.map +1 -0
  85. package/dist/council/personas.js +44 -0
  86. package/dist/council/personas.js.map +1 -0
  87. package/dist/council/reviewers.d.ts +13 -0
  88. package/dist/council/reviewers.d.ts.map +1 -0
  89. package/dist/council/reviewers.js +59 -0
  90. package/dist/council/reviewers.js.map +1 -0
  91. package/dist/council/risk.d.ts +16 -0
  92. package/dist/council/risk.d.ts.map +1 -0
  93. package/dist/council/risk.js +74 -0
  94. package/dist/council/risk.js.map +1 -0
  95. package/dist/council/types.d.ts +95 -0
  96. package/dist/council/types.d.ts.map +1 -0
  97. package/dist/council/types.js +2 -0
  98. package/dist/council/types.js.map +1 -0
  99. package/dist/jury/evaluate.d.ts +13 -0
  100. package/dist/jury/evaluate.d.ts.map +1 -0
  101. package/{modules/jury/evaluate.ts → dist/jury/evaluate.js} +60 -84
  102. package/dist/jury/evaluate.js.map +1 -0
  103. package/dist/jury/index.d.ts +6 -0
  104. package/dist/jury/index.d.ts.map +1 -0
  105. package/dist/jury/index.js +4 -0
  106. package/dist/jury/index.js.map +1 -0
  107. package/dist/jury/preflight.d.ts +26 -0
  108. package/dist/jury/preflight.d.ts.map +1 -0
  109. package/dist/jury/preflight.js +71 -0
  110. package/dist/jury/preflight.js.map +1 -0
  111. package/dist/jury/schema.d.ts +57 -0
  112. package/dist/jury/schema.d.ts.map +1 -0
  113. package/dist/jury/schema.js +21 -0
  114. package/dist/jury/schema.js.map +1 -0
  115. package/dist/jury/types.d.ts +47 -0
  116. package/dist/jury/types.d.ts.map +1 -0
  117. package/dist/jury/types.js +2 -0
  118. package/dist/jury/types.js.map +1 -0
  119. package/dist/oracle/adapters/lance-db.d.ts +15 -0
  120. package/dist/oracle/adapters/lance-db.d.ts.map +1 -0
  121. package/dist/oracle/adapters/lance-db.js +68 -0
  122. package/dist/oracle/adapters/lance-db.js.map +1 -0
  123. package/dist/oracle/adapters/xenova-embedder.d.ts +21 -0
  124. package/dist/oracle/adapters/xenova-embedder.d.ts.map +1 -0
  125. package/dist/oracle/adapters/xenova-embedder.js +36 -0
  126. package/dist/oracle/adapters/xenova-embedder.js.map +1 -0
  127. package/dist/oracle/bm25.d.ts +20 -0
  128. package/dist/oracle/bm25.d.ts.map +1 -0
  129. package/dist/oracle/bm25.js +82 -0
  130. package/dist/oracle/bm25.js.map +1 -0
  131. package/dist/oracle/index.d.ts +21 -0
  132. package/dist/oracle/index.d.ts.map +1 -0
  133. package/dist/oracle/index.js +25 -0
  134. package/dist/oracle/index.js.map +1 -0
  135. package/dist/oracle/log.d.ts +6 -0
  136. package/dist/oracle/log.d.ts.map +1 -0
  137. package/dist/oracle/log.js +12 -0
  138. package/dist/oracle/log.js.map +1 -0
  139. package/dist/oracle/propose.d.ts +25 -0
  140. package/dist/oracle/propose.d.ts.map +1 -0
  141. package/dist/oracle/propose.js +133 -0
  142. package/dist/oracle/propose.js.map +1 -0
  143. package/dist/oracle/query.d.ts +17 -0
  144. package/dist/oracle/query.d.ts.map +1 -0
  145. package/dist/oracle/query.js +106 -0
  146. package/dist/oracle/query.js.map +1 -0
  147. package/dist/oracle/summary.d.ts +11 -0
  148. package/dist/oracle/summary.d.ts.map +1 -0
  149. package/dist/oracle/summary.js +102 -0
  150. package/dist/oracle/summary.js.map +1 -0
  151. package/dist/oracle/types.d.ts +31 -0
  152. package/dist/oracle/types.d.ts.map +1 -0
  153. package/dist/oracle/types.js +2 -0
  154. package/dist/oracle/types.js.map +1 -0
  155. package/dist/sentinel/assert.d.ts +28 -0
  156. package/dist/sentinel/assert.d.ts.map +1 -0
  157. package/dist/sentinel/assert.js +63 -0
  158. package/dist/sentinel/assert.js.map +1 -0
  159. package/dist/sentinel/coverage.d.ts +14 -0
  160. package/dist/sentinel/coverage.d.ts.map +1 -0
  161. package/dist/sentinel/coverage.js +96 -0
  162. package/dist/sentinel/coverage.js.map +1 -0
  163. package/dist/sentinel/drift.d.ts +12 -0
  164. package/dist/sentinel/drift.d.ts.map +1 -0
  165. package/dist/sentinel/drift.js +149 -0
  166. package/dist/sentinel/drift.js.map +1 -0
  167. package/dist/sentinel/index.d.ts +7 -0
  168. package/dist/sentinel/index.d.ts.map +1 -0
  169. package/dist/sentinel/index.js +5 -0
  170. package/dist/sentinel/index.js.map +1 -0
  171. package/dist/sentinel/review.d.ts +15 -0
  172. package/dist/sentinel/review.d.ts.map +1 -0
  173. package/dist/sentinel/review.js +177 -0
  174. package/dist/sentinel/review.js.map +1 -0
  175. package/dist/setup.d.ts +103 -0
  176. package/dist/setup.d.ts.map +1 -0
  177. package/dist/setup.js +87 -0
  178. package/dist/setup.js.map +1 -0
  179. package/dist/shared/types.d.ts +173 -0
  180. package/dist/shared/types.d.ts.map +1 -0
  181. package/dist/shared/types.js +16 -0
  182. package/dist/shared/types.js.map +1 -0
  183. package/package.json +13 -8
  184. package/.github/copilot-instructions.md +0 -117
  185. package/CLAUDE.md +0 -146
  186. package/GEMINI.md +0 -73
  187. package/SETUP.md +0 -264
  188. package/evals/__tests__/eval.test.ts +0 -31
  189. package/evals/cases/auth_hs256_rejected.json +0 -46
  190. package/evals/cases/auth_rs256_valid.json +0 -30
  191. package/evals/cases/cache_missing_lock.json +0 -31
  192. package/evals/cases/db_naive_not_null.json +0 -32
  193. package/evals/cases/logging_pii_leak.json +0 -32
  194. package/evals/cases/migration_with_rollback.json +0 -43
  195. package/evals/cases/no_evidence_novel_design.json +0 -16
  196. package/evals/cases/payment_no_idempotency.json +0 -33
  197. package/evals/cases/redis_session_rejected.json +0 -32
  198. package/evals/cases/safe_refactor.json +0 -17
  199. package/evals/runner.ts +0 -226
  200. package/modules/AGENTS.md +0 -78
  201. package/modules/CLAUDE.md +0 -93
  202. package/modules/README.md +0 -504
  203. package/modules/advisor/ask.ts +0 -87
  204. package/modules/advisor/index.ts +0 -2
  205. package/modules/advisor/types.ts +0 -26
  206. package/modules/compass/behavior.ts +0 -161
  207. package/modules/compass/create.ts +0 -365
  208. package/modules/compass/evidence/collect.ts +0 -109
  209. package/modules/compass/index.ts +0 -7
  210. package/modules/compass/propose.ts +0 -152
  211. package/modules/compass/schemas.ts +0 -121
  212. package/modules/compass/score.ts +0 -77
  213. package/modules/compass/sources/index.ts +0 -413
  214. package/modules/compass/types.ts +0 -431
  215. package/modules/council/advisors.ts +0 -71
  216. package/modules/council/chairman.ts +0 -183
  217. package/modules/council/deliberate.ts +0 -141
  218. package/modules/council/frame.ts +0 -54
  219. package/modules/council/index.ts +0 -9
  220. package/modules/council/personas.ts +0 -57
  221. package/modules/council/reviewers.ts +0 -82
  222. package/modules/council/risk.ts +0 -89
  223. package/modules/council/types.ts +0 -107
  224. package/modules/jury/index.ts +0 -5
  225. package/modules/jury/preflight.ts +0 -101
  226. package/modules/jury/schema.ts +0 -24
  227. package/modules/jury/types.ts +0 -50
  228. package/modules/oracle/adapters/lance-db.ts +0 -81
  229. package/modules/oracle/adapters/xenova-embedder.ts +0 -43
  230. package/modules/oracle/bm25.ts +0 -92
  231. package/modules/oracle/index.ts +0 -36
  232. package/modules/oracle/log.ts +0 -15
  233. package/modules/oracle/propose.ts +0 -164
  234. package/modules/oracle/query.ts +0 -146
  235. package/modules/oracle/summary.ts +0 -116
  236. package/modules/oracle/types.ts +0 -32
  237. package/modules/sentinel/assert.ts +0 -95
  238. package/modules/sentinel/coverage.ts +0 -106
  239. package/modules/sentinel/drift.ts +0 -163
  240. package/modules/sentinel/index.ts +0 -6
  241. package/modules/sentinel/review.ts +0 -208
  242. package/modules/setup.ts +0 -202
  243. 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
- }
@@ -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"