sverklo 0.2.12 → 0.2.14
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 +102 -13
- package/dist/bin/sverklo.js +69 -1
- package/dist/bin/sverklo.js.map +1 -1
- package/dist/src/audit-prompt.d.ts +2 -0
- package/dist/src/audit-prompt.js +91 -0
- package/dist/src/audit-prompt.js.map +1 -0
- package/dist/src/doctor.js +55 -0
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/indexer/embedding-providers.d.ts +34 -0
- package/dist/src/indexer/embedding-providers.js +215 -0
- package/dist/src/indexer/embedding-providers.js.map +1 -0
- package/dist/src/indexer/embedding-providers.test.d.ts +1 -0
- package/dist/src/indexer/embedding-providers.test.js +83 -0
- package/dist/src/indexer/embedding-providers.test.js.map +1 -0
- package/dist/src/indexer/indexer-freshness.test.d.ts +1 -0
- package/dist/src/indexer/indexer-freshness.test.js +109 -0
- package/dist/src/indexer/indexer-freshness.test.js.map +1 -0
- package/dist/src/indexer/indexer-provider-integration.test.d.ts +1 -0
- package/dist/src/indexer/indexer-provider-integration.test.js +142 -0
- package/dist/src/indexer/indexer-provider-integration.test.js.map +1 -0
- package/dist/src/indexer/indexer.d.ts +44 -0
- package/dist/src/indexer/indexer.js +153 -10
- package/dist/src/indexer/indexer.js.map +1 -1
- package/dist/src/indexer/migration.test.d.ts +1 -0
- package/dist/src/indexer/migration.test.js +144 -0
- package/dist/src/indexer/migration.test.js.map +1 -0
- package/dist/src/indexer/symbol-extractor.js +17 -5
- package/dist/src/indexer/symbol-extractor.js.map +1 -1
- package/dist/src/indexer/symbol-extractor.test.d.ts +1 -0
- package/dist/src/indexer/symbol-extractor.test.js +88 -0
- package/dist/src/indexer/symbol-extractor.test.js.map +1 -0
- package/dist/src/indexer/watcher.js +4 -0
- package/dist/src/indexer/watcher.js.map +1 -1
- package/dist/src/memory/journal.d.ts +62 -0
- package/dist/src/memory/journal.js +136 -0
- package/dist/src/memory/journal.js.map +1 -0
- package/dist/src/memory/journal.test.d.ts +1 -0
- package/dist/src/memory/journal.test.js +98 -0
- package/dist/src/memory/journal.test.js.map +1 -0
- package/dist/src/modes.d.ts +16 -0
- package/dist/src/modes.js +104 -0
- package/dist/src/modes.js.map +1 -0
- package/dist/src/modes.test.d.ts +1 -0
- package/dist/src/modes.test.js +48 -0
- package/dist/src/modes.test.js.map +1 -0
- package/dist/src/search/hybrid-search.d.ts +22 -0
- package/dist/src/search/hybrid-search.js +169 -7
- package/dist/src/search/hybrid-search.js.map +1 -1
- package/dist/src/search/hybrid-search.test.d.ts +1 -0
- package/dist/src/search/hybrid-search.test.js +112 -0
- package/dist/src/search/hybrid-search.test.js.map +1 -0
- package/dist/src/search/token-budget.d.ts +1 -0
- package/dist/src/search/token-budget.js +34 -6
- package/dist/src/search/token-budget.js.map +1 -1
- package/dist/src/server/mcp-server.js +32 -25
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/server/tool-overrides.d.ts +16 -0
- package/dist/src/server/tool-overrides.js +102 -0
- package/dist/src/server/tool-overrides.js.map +1 -0
- package/dist/src/server/tool-overrides.test.d.ts +1 -0
- package/dist/src/server/tool-overrides.test.js +124 -0
- package/dist/src/server/tool-overrides.test.js.map +1 -0
- package/dist/src/server/tools/context-budget.test.d.ts +1 -0
- package/dist/src/server/tools/context-budget.test.js +150 -0
- package/dist/src/server/tools/context-budget.test.js.map +1 -0
- package/dist/src/server/tools/context.d.ts +11 -1
- package/dist/src/server/tools/context.js +202 -8
- package/dist/src/server/tools/context.js.map +1 -1
- package/dist/src/server/tools/diff-heuristics.d.ts +28 -0
- package/dist/src/server/tools/diff-heuristics.js +213 -0
- package/dist/src/server/tools/diff-heuristics.js.map +1 -0
- package/dist/src/server/tools/diff-heuristics.test.d.ts +1 -0
- package/dist/src/server/tools/diff-heuristics.test.js +151 -0
- package/dist/src/server/tools/diff-heuristics.test.js.map +1 -0
- package/dist/src/server/tools/find-references.d.ts +4 -0
- package/dist/src/server/tools/find-references.js +46 -6
- package/dist/src/server/tools/find-references.js.map +1 -1
- package/dist/src/server/tools/find-references.test.d.ts +1 -0
- package/dist/src/server/tools/find-references.test.js +98 -0
- package/dist/src/server/tools/find-references.test.js.map +1 -0
- package/dist/src/server/tools/forget.js +3 -0
- package/dist/src/server/tools/forget.js.map +1 -1
- package/dist/src/server/tools/index-status.js +16 -0
- package/dist/src/server/tools/index-status.js.map +1 -1
- package/dist/src/server/tools/lookup.js +21 -13
- package/dist/src/server/tools/lookup.js.map +1 -1
- package/dist/src/server/tools/lookup.test.d.ts +1 -0
- package/dist/src/server/tools/lookup.test.js +110 -0
- package/dist/src/server/tools/lookup.test.js.map +1 -0
- package/dist/src/server/tools/recall.d.ts +5 -1
- package/dist/src/server/tools/recall.js +68 -6
- package/dist/src/server/tools/recall.js.map +1 -1
- package/dist/src/server/tools/recall.test.d.ts +1 -0
- package/dist/src/server/tools/recall.test.js +116 -0
- package/dist/src/server/tools/recall.test.js.map +1 -0
- package/dist/src/server/tools/remember.js +16 -2
- package/dist/src/server/tools/remember.js.map +1 -1
- package/dist/src/server/tools/review-diff.js +31 -0
- package/dist/src/server/tools/review-diff.js.map +1 -1
- package/dist/src/server/tools/search.js +29 -4
- package/dist/src/server/tools/search.js.map +1 -1
- package/dist/src/server/tools/search.test.d.ts +1 -0
- package/dist/src/server/tools/search.test.js +118 -0
- package/dist/src/server/tools/search.test.js.map +1 -0
- package/dist/src/server/tools/tier.js +25 -1
- package/dist/src/server/tools/tier.js.map +1 -1
- package/dist/src/storage/chunk-store.d.ts +12 -0
- package/dist/src/storage/chunk-store.js +16 -0
- package/dist/src/storage/chunk-store.js.map +1 -1
- package/dist/src/storage/chunk-store.test.d.ts +1 -0
- package/dist/src/storage/chunk-store.test.js +69 -0
- package/dist/src/storage/chunk-store.test.js.map +1 -0
- package/dist/src/storage/database.d.ts +13 -0
- package/dist/src/storage/database.js +47 -0
- package/dist/src/storage/database.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractReferences } from "./symbol-extractor.js";
|
|
3
|
+
// Regression test for github.com/sverklo/sverklo/issues/13.
|
|
4
|
+
//
|
|
5
|
+
// The old implementation used a chunk-wide `seen` set to dedupe
|
|
6
|
+
// references. That made sverklo_impact lossy: a symbol called N
|
|
7
|
+
// times in the same function returned only one row, pointing at
|
|
8
|
+
// the first call site. For refactor safety — the whole point of
|
|
9
|
+
// the impact tool — this was the worst possible failure mode:
|
|
10
|
+
// the user trusted the output, made the refactor, missed N-1 real
|
|
11
|
+
// call sites, shipped the bug.
|
|
12
|
+
//
|
|
13
|
+
// The fix: per-line dedupe (still prevents double-counting when
|
|
14
|
+
// two regexes both fire on the same line, e.g. `new Foo()` or
|
|
15
|
+
// `Foo(Foo())`) while letting repeat calls across lines each
|
|
16
|
+
// contribute their own symbol_ref row.
|
|
17
|
+
describe("extractReferences — issue #13 regression", () => {
|
|
18
|
+
it("records multiple call sites of the same symbol across lines", () => {
|
|
19
|
+
const content = [
|
|
20
|
+
"function index() {",
|
|
21
|
+
" const vectors = await embed(texts);",
|
|
22
|
+
" // ...",
|
|
23
|
+
" const [vector] = await embed([text]);",
|
|
24
|
+
" return vectors;",
|
|
25
|
+
"}",
|
|
26
|
+
].join("\n");
|
|
27
|
+
const refs = extractReferences(content, "index");
|
|
28
|
+
const embedRefs = refs.filter((r) => r.name === "embed");
|
|
29
|
+
expect(embedRefs.length).toBe(2);
|
|
30
|
+
expect(embedRefs[0].line).toBe(1);
|
|
31
|
+
expect(embedRefs[1].line).toBe(3);
|
|
32
|
+
});
|
|
33
|
+
it("still dedupes the same symbol on the same line (regex collision guard)", () => {
|
|
34
|
+
// A line with two occurrences of the same identifier should
|
|
35
|
+
// still produce only one ref entry — per-line dedupe survives.
|
|
36
|
+
const content = ["function outer() {", " return foo(foo(x));", "}"].join("\n");
|
|
37
|
+
const refs = extractReferences(content, "outer");
|
|
38
|
+
const fooRefs = refs.filter((r) => r.name === "foo");
|
|
39
|
+
// Exactly one foo ref on line 1 — not two from the nested call.
|
|
40
|
+
expect(fooRefs.length).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
it("records calls in distinct branches as distinct refs", () => {
|
|
43
|
+
const content = [
|
|
44
|
+
"function handle(x) {",
|
|
45
|
+
" if (x > 0) {",
|
|
46
|
+
" publish(x);",
|
|
47
|
+
" } else {",
|
|
48
|
+
" publish(null);",
|
|
49
|
+
" }",
|
|
50
|
+
"}",
|
|
51
|
+
].join("\n");
|
|
52
|
+
const refs = extractReferences(content, "handle");
|
|
53
|
+
const publishRefs = refs.filter((r) => r.name === "publish");
|
|
54
|
+
expect(publishRefs.length).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
it("does not record a self-reference when selfName matches", () => {
|
|
57
|
+
const content = ["function fact(n) {", " return n * fact(n - 1);", "}"].join("\n");
|
|
58
|
+
const refs = extractReferences(content, "fact");
|
|
59
|
+
expect(refs.filter((r) => r.name === "fact").length).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
it("strips string literals before matching", () => {
|
|
62
|
+
// `foo` in a string is not a real call — must not be recorded.
|
|
63
|
+
const content = [
|
|
64
|
+
"function log() {",
|
|
65
|
+
" const msg = 'foo was called here';",
|
|
66
|
+
' const other = "foo";',
|
|
67
|
+
" foo();",
|
|
68
|
+
"}",
|
|
69
|
+
].join("\n");
|
|
70
|
+
const refs = extractReferences(content, "log");
|
|
71
|
+
const fooRefs = refs.filter((r) => r.name === "foo");
|
|
72
|
+
// Only the real call on the last line.
|
|
73
|
+
expect(fooRefs.length).toBe(1);
|
|
74
|
+
expect(fooRefs[0].line).toBe(3);
|
|
75
|
+
});
|
|
76
|
+
it("strips line comments before matching", () => {
|
|
77
|
+
const content = [
|
|
78
|
+
"function test() {",
|
|
79
|
+
" // foo() is not actually called here",
|
|
80
|
+
" bar();",
|
|
81
|
+
"}",
|
|
82
|
+
].join("\n");
|
|
83
|
+
const refs = extractReferences(content, "test");
|
|
84
|
+
expect(refs.some((r) => r.name === "foo")).toBe(false);
|
|
85
|
+
expect(refs.some((r) => r.name === "bar")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=symbol-extractor.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"symbol-extractor.test.js","sourceRoot":"","sources":["../../../src/indexer/symbol-extractor.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,6 +29,9 @@ export function startWatcher(indexer, rootPath) {
|
|
|
29
29
|
const lang = detectLanguage(absolutePath);
|
|
30
30
|
if (!lang)
|
|
31
31
|
return;
|
|
32
|
+
// Any real change invalidates the freshness cache immediately so the
|
|
33
|
+
// next sverklo_status reflects reality without waiting for the TTL.
|
|
34
|
+
indexer.invalidateFreshnessCache();
|
|
32
35
|
// Debounce
|
|
33
36
|
const existing = pending.get(rel);
|
|
34
37
|
if (existing)
|
|
@@ -44,6 +47,7 @@ export function startWatcher(indexer, rootPath) {
|
|
|
44
47
|
watcher.on("unlink", (absolutePath) => {
|
|
45
48
|
const rel = relative(rootPath, absolutePath);
|
|
46
49
|
log(`File removed: ${rel}`);
|
|
50
|
+
indexer.invalidateFreshnessCache();
|
|
47
51
|
indexer.removeFile(rel);
|
|
48
52
|
});
|
|
49
53
|
log("File watcher started");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../../src/indexer/watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGzC,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,QAAgB;IAC7D,MAAM,YAAY,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAElD,gCAAgC;IAChC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAClD,MAAM,WAAW,GAAG,GAAG,CAAC;IAExB,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE;QAC9B,OAAO,EAAE,CAAC,IAAY,EAAE,EAAE;YACxB,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACrC,IAAI,CAAC,GAAG;gBAAE,OAAO,KAAK,CAAC;YACvB,IAAI,CAAC;gBACH,OAAO,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QACD,UAAU,EAAE,IAAI;QAChB,aAAa,EAAE,IAAI;QACnB,gBAAgB,EAAE,EAAE,kBAAkB,EAAE,GAAG,EAAE;KAC9C,CAAC,CAAC;IAEH,SAAS,YAAY,CAAC,YAAoB;QACxC,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,WAAW;QACX,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,QAAQ;YAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QAErC,OAAO,CAAC,GAAG,CACT,GAAG,EACH,UAAU,CAAC,KAAK,IAAI,EAAE;YACpB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,GAAG,CAAC,iBAAiB,GAAG,EAAE,CAAC,CAAC;YAC5B,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;QACrD,CAAC,EAAE,WAAW,CAAC,CAChB,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,YAAoB,EAAE,EAAE;QAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC7C,GAAG,CAAC,iBAAiB,GAAG,EAAE,CAAC,CAAC;QAC5B,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,sBAAsB,CAAC,CAAC;AAC9B,CAAC"}
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../../src/indexer/watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGzC,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,QAAgB;IAC7D,MAAM,YAAY,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAElD,gCAAgC;IAChC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAClD,MAAM,WAAW,GAAG,GAAG,CAAC;IAExB,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE;QAC9B,OAAO,EAAE,CAAC,IAAY,EAAE,EAAE;YACxB,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACrC,IAAI,CAAC,GAAG;gBAAE,OAAO,KAAK,CAAC;YACvB,IAAI,CAAC;gBACH,OAAO,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QACD,UAAU,EAAE,IAAI;QAChB,aAAa,EAAE,IAAI;QACnB,gBAAgB,EAAE,EAAE,kBAAkB,EAAE,GAAG,EAAE;KAC9C,CAAC,CAAC;IAEH,SAAS,YAAY,CAAC,YAAoB;QACxC,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,qEAAqE;QACrE,oEAAoE;QACpE,OAAO,CAAC,wBAAwB,EAAE,CAAC;QAEnC,WAAW;QACX,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,QAAQ;YAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QAErC,OAAO,CAAC,GAAG,CACT,GAAG,EACH,UAAU,CAAC,KAAK,IAAI,EAAE;YACpB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,GAAG,CAAC,iBAAiB,GAAG,EAAE,CAAC,CAAC;YAC5B,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;QACrD,CAAC,EAAE,WAAW,CAAC,CAChB,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,YAAoB,EAAE,EAAE;QAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC7C,GAAG,CAAC,iBAAiB,GAAG,EAAE,CAAC,CAAC;QAC5B,OAAO,CAAC,wBAAwB,EAAE,CAAC;QACnC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,sBAAsB,CAAC,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type JournalOp = "remember" | "forget" | "invalidate" | "promote" | "demote" | "touch";
|
|
2
|
+
export interface JournalEntry {
|
|
3
|
+
op: JournalOp;
|
|
4
|
+
id: number;
|
|
5
|
+
ts: string;
|
|
6
|
+
content?: string;
|
|
7
|
+
category?: string;
|
|
8
|
+
tags?: string[] | null;
|
|
9
|
+
confidence?: number;
|
|
10
|
+
git_sha?: string | null;
|
|
11
|
+
git_branch?: string | null;
|
|
12
|
+
related_files?: string[] | null;
|
|
13
|
+
tier?: string;
|
|
14
|
+
invalidated_at_sha?: string | null;
|
|
15
|
+
replaced_by_id?: number | null;
|
|
16
|
+
new_tier?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* MemoryJournal writes append-only JSONL to <project>/.sverklo/memories.jsonl.
|
|
20
|
+
*
|
|
21
|
+
* Writes are synchronous on purpose: the journal must be committed
|
|
22
|
+
* before the corresponding SQLite write returns to the caller, so we
|
|
23
|
+
* never have a window where SQLite succeeds and the journal is missing
|
|
24
|
+
* the record. Agent workloads are low-frequency on the memory side
|
|
25
|
+
* (dozens of writes per session, not thousands) so sync writes are
|
|
26
|
+
* fine. If memory write volume grows we can batch.
|
|
27
|
+
*
|
|
28
|
+
* Errors are swallowed with a log. The journal is advisory for 0.2.x —
|
|
29
|
+
* a broken journal must not break memory.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MemoryJournal {
|
|
32
|
+
private path;
|
|
33
|
+
private ready;
|
|
34
|
+
constructor(projectRoot: string);
|
|
35
|
+
private ensureDir;
|
|
36
|
+
private write;
|
|
37
|
+
remember(params: {
|
|
38
|
+
id: number;
|
|
39
|
+
content: string;
|
|
40
|
+
category: string;
|
|
41
|
+
tags?: string[] | null;
|
|
42
|
+
confidence: number;
|
|
43
|
+
git_sha?: string | null;
|
|
44
|
+
git_branch?: string | null;
|
|
45
|
+
related_files?: string[] | null;
|
|
46
|
+
tier: string;
|
|
47
|
+
}): void;
|
|
48
|
+
forget(id: number): void;
|
|
49
|
+
invalidate(id: number, atSha: string | null, replacedById: number | null): void;
|
|
50
|
+
promote(id: number, newTier: string): void;
|
|
51
|
+
demote(id: number, newTier: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Return the path to the journal file so callers can show it in
|
|
54
|
+
* error messages ("see .sverklo/memories.jsonl for the write log").
|
|
55
|
+
*/
|
|
56
|
+
get filePath(): string;
|
|
57
|
+
/**
|
|
58
|
+
* True if the journal file exists on disk. Useful for the doctor
|
|
59
|
+
* command to report journal presence.
|
|
60
|
+
*/
|
|
61
|
+
exists(): boolean;
|
|
62
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Append-only JSONL journal for memory writes (issue #7).
|
|
2
|
+
//
|
|
3
|
+
// Sverklo's memory layer lives in SQLite for query speed, but users
|
|
4
|
+
// can't easily inspect, diff, or sync a binary database. The Memory
|
|
5
|
+
// MCP reference server showed that append-only JSONL is the right
|
|
6
|
+
// format for user-facing memory: human-readable, git-diffable,
|
|
7
|
+
// trivially syncable, and impossible to corrupt in interesting ways.
|
|
8
|
+
//
|
|
9
|
+
// This module ships the JSONL format as a *mirror* of SQLite, not a
|
|
10
|
+
// replacement. Every memory write (remember / forget / promote /
|
|
11
|
+
// demote / invalidate) lands in both places. If the two ever
|
|
12
|
+
// disagree, SQLite is still the source of truth for 0.2.x — the
|
|
13
|
+
// migration to "JSONL as source of truth" is a later, more invasive
|
|
14
|
+
// step tracked in the same issue.
|
|
15
|
+
//
|
|
16
|
+
// Why mirror, not replace, in this step:
|
|
17
|
+
// - Zero risk to existing users. If the journal is broken, memory
|
|
18
|
+
// still works from SQLite.
|
|
19
|
+
// - Users get the user-visible benefit today (cat / git diff /
|
|
20
|
+
// grep the journal) without waiting for the full migration.
|
|
21
|
+
// - The journal can be replayed into a fresh SQLite if the index
|
|
22
|
+
// is ever deleted — a cheap disaster-recovery path.
|
|
23
|
+
//
|
|
24
|
+
// Format: one JSON object per line, one operation per object. Writes
|
|
25
|
+
// are append-only. Tombstones (forget) are records, not file rewrites.
|
|
26
|
+
// Compaction is a later feature — for now we just grow.
|
|
27
|
+
//
|
|
28
|
+
// File location: `<project>/.sverklo/memories.jsonl`. This lives
|
|
29
|
+
// inside the project root on purpose so users can commit it alongside
|
|
30
|
+
// their code. `.sverklo/index.db` and friends stay in `~/.sverklo/`
|
|
31
|
+
// (the user cache) because those are machine-specific.
|
|
32
|
+
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
import { logError } from "../utils/logger.js";
|
|
35
|
+
/**
|
|
36
|
+
* MemoryJournal writes append-only JSONL to <project>/.sverklo/memories.jsonl.
|
|
37
|
+
*
|
|
38
|
+
* Writes are synchronous on purpose: the journal must be committed
|
|
39
|
+
* before the corresponding SQLite write returns to the caller, so we
|
|
40
|
+
* never have a window where SQLite succeeds and the journal is missing
|
|
41
|
+
* the record. Agent workloads are low-frequency on the memory side
|
|
42
|
+
* (dozens of writes per session, not thousands) so sync writes are
|
|
43
|
+
* fine. If memory write volume grows we can batch.
|
|
44
|
+
*
|
|
45
|
+
* Errors are swallowed with a log. The journal is advisory for 0.2.x —
|
|
46
|
+
* a broken journal must not break memory.
|
|
47
|
+
*/
|
|
48
|
+
export class MemoryJournal {
|
|
49
|
+
path;
|
|
50
|
+
ready = false;
|
|
51
|
+
constructor(projectRoot) {
|
|
52
|
+
this.path = join(projectRoot, ".sverklo", "memories.jsonl");
|
|
53
|
+
}
|
|
54
|
+
ensureDir() {
|
|
55
|
+
if (this.ready)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
59
|
+
this.ready = true;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logError("MemoryJournal: could not create .sverklo directory", err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
write(entry) {
|
|
66
|
+
this.ensureDir();
|
|
67
|
+
try {
|
|
68
|
+
appendFileSync(this.path, JSON.stringify(entry) + "\n", "utf-8");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logError("MemoryJournal: append failed", err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
remember(params) {
|
|
75
|
+
this.write({
|
|
76
|
+
op: "remember",
|
|
77
|
+
id: params.id,
|
|
78
|
+
ts: new Date().toISOString(),
|
|
79
|
+
content: params.content,
|
|
80
|
+
category: params.category,
|
|
81
|
+
tags: params.tags ?? null,
|
|
82
|
+
confidence: params.confidence,
|
|
83
|
+
git_sha: params.git_sha ?? null,
|
|
84
|
+
git_branch: params.git_branch ?? null,
|
|
85
|
+
related_files: params.related_files ?? null,
|
|
86
|
+
tier: params.tier,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
forget(id) {
|
|
90
|
+
this.write({
|
|
91
|
+
op: "forget",
|
|
92
|
+
id,
|
|
93
|
+
ts: new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
invalidate(id, atSha, replacedById) {
|
|
97
|
+
this.write({
|
|
98
|
+
op: "invalidate",
|
|
99
|
+
id,
|
|
100
|
+
ts: new Date().toISOString(),
|
|
101
|
+
invalidated_at_sha: atSha ?? null,
|
|
102
|
+
replaced_by_id: replacedById ?? null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
promote(id, newTier) {
|
|
106
|
+
this.write({
|
|
107
|
+
op: "promote",
|
|
108
|
+
id,
|
|
109
|
+
ts: new Date().toISOString(),
|
|
110
|
+
new_tier: newTier,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
demote(id, newTier) {
|
|
114
|
+
this.write({
|
|
115
|
+
op: "demote",
|
|
116
|
+
id,
|
|
117
|
+
ts: new Date().toISOString(),
|
|
118
|
+
new_tier: newTier,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Return the path to the journal file so callers can show it in
|
|
123
|
+
* error messages ("see .sverklo/memories.jsonl for the write log").
|
|
124
|
+
*/
|
|
125
|
+
get filePath() {
|
|
126
|
+
return this.path;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* True if the journal file exists on disk. Useful for the doctor
|
|
130
|
+
* command to report journal presence.
|
|
131
|
+
*/
|
|
132
|
+
exists() {
|
|
133
|
+
return existsSync(this.path);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal.js","sourceRoot":"","sources":["../../../src/memory/journal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { MemoryJournal } from "./journal.js";
|
|
6
|
+
describe("MemoryJournal", () => {
|
|
7
|
+
let projectRoot;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
projectRoot = mkdtempSync(join(tmpdir(), "sverklo-journal-"));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
try {
|
|
13
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
});
|
|
17
|
+
it("creates the .sverklo directory and writes an append-only file", () => {
|
|
18
|
+
const j = new MemoryJournal(projectRoot);
|
|
19
|
+
j.remember({
|
|
20
|
+
id: 1,
|
|
21
|
+
content: "All timestamps are UTC.",
|
|
22
|
+
category: "procedural",
|
|
23
|
+
tags: ["timezone"],
|
|
24
|
+
confidence: 1.0,
|
|
25
|
+
git_sha: "abc123",
|
|
26
|
+
git_branch: "main",
|
|
27
|
+
related_files: ["src/time.ts"],
|
|
28
|
+
tier: "core",
|
|
29
|
+
});
|
|
30
|
+
const path = join(projectRoot, ".sverklo", "memories.jsonl");
|
|
31
|
+
expect(existsSync(path)).toBe(true);
|
|
32
|
+
const content = readFileSync(path, "utf-8");
|
|
33
|
+
expect(content.split("\n").filter(Boolean).length).toBe(1);
|
|
34
|
+
const entry = JSON.parse(content.trim());
|
|
35
|
+
expect(entry.op).toBe("remember");
|
|
36
|
+
expect(entry.id).toBe(1);
|
|
37
|
+
expect(entry.content).toBe("All timestamps are UTC.");
|
|
38
|
+
expect(entry.tier).toBe("core");
|
|
39
|
+
expect(entry.git_sha).toBe("abc123");
|
|
40
|
+
expect(typeof entry.ts).toBe("string");
|
|
41
|
+
});
|
|
42
|
+
it("appends multiple operations in order", () => {
|
|
43
|
+
const j = new MemoryJournal(projectRoot);
|
|
44
|
+
j.remember({
|
|
45
|
+
id: 1,
|
|
46
|
+
content: "First",
|
|
47
|
+
category: "decision",
|
|
48
|
+
confidence: 1.0,
|
|
49
|
+
tier: "archive",
|
|
50
|
+
});
|
|
51
|
+
j.promote(1, "core");
|
|
52
|
+
j.forget(1);
|
|
53
|
+
const path = join(projectRoot, ".sverklo", "memories.jsonl");
|
|
54
|
+
const lines = readFileSync(path, "utf-8").trim().split("\n");
|
|
55
|
+
expect(lines.length).toBe(3);
|
|
56
|
+
const ops = lines.map((l) => JSON.parse(l).op);
|
|
57
|
+
expect(ops).toEqual(["remember", "promote", "forget"]);
|
|
58
|
+
});
|
|
59
|
+
it("records tombstones for forget so the journal stays replayable", () => {
|
|
60
|
+
const j = new MemoryJournal(projectRoot);
|
|
61
|
+
j.remember({ id: 5, content: "x", category: "context", confidence: 1.0, tier: "archive" });
|
|
62
|
+
j.forget(5);
|
|
63
|
+
const path = join(projectRoot, ".sverklo", "memories.jsonl");
|
|
64
|
+
const content = readFileSync(path, "utf-8");
|
|
65
|
+
// Second line is the tombstone — forget must not rewrite/truncate.
|
|
66
|
+
expect(content).toContain('"op":"remember"');
|
|
67
|
+
expect(content).toContain('"op":"forget"');
|
|
68
|
+
});
|
|
69
|
+
it("records invalidate with atSha and replacedById", () => {
|
|
70
|
+
const j = new MemoryJournal(projectRoot);
|
|
71
|
+
j.invalidate(3, "def456", 7);
|
|
72
|
+
const path = join(projectRoot, ".sverklo", "memories.jsonl");
|
|
73
|
+
const entry = JSON.parse(readFileSync(path, "utf-8").trim());
|
|
74
|
+
expect(entry.op).toBe("invalidate");
|
|
75
|
+
expect(entry.id).toBe(3);
|
|
76
|
+
expect(entry.invalidated_at_sha).toBe("def456");
|
|
77
|
+
expect(entry.replaced_by_id).toBe(7);
|
|
78
|
+
});
|
|
79
|
+
it("is robust to optional fields being undefined", () => {
|
|
80
|
+
const j = new MemoryJournal(projectRoot);
|
|
81
|
+
j.remember({
|
|
82
|
+
id: 2,
|
|
83
|
+
content: "minimal",
|
|
84
|
+
category: "context",
|
|
85
|
+
confidence: 1.0,
|
|
86
|
+
tier: "archive",
|
|
87
|
+
});
|
|
88
|
+
const entry = JSON.parse(readFileSync(join(projectRoot, ".sverklo", "memories.jsonl"), "utf-8").trim());
|
|
89
|
+
expect(entry.tags).toBeNull();
|
|
90
|
+
expect(entry.git_sha).toBeNull();
|
|
91
|
+
expect(entry.related_files).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
it("exposes the file path via filePath getter", () => {
|
|
94
|
+
const j = new MemoryJournal(projectRoot);
|
|
95
|
+
expect(j.filePath).toBe(join(projectRoot, ".sverklo", "memories.jsonl"));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=journal.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal.test.js","sourceRoot":"","sources":["../../../src/memory/journal.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SverkloMode = "embedded" | "shared" | "cloud";
|
|
2
|
+
export interface ModeResolution {
|
|
3
|
+
mode: SverkloMode;
|
|
4
|
+
reason: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveMode(argv: string[], env?: NodeJS.ProcessEnv): ModeResolution;
|
|
7
|
+
export declare class SverkloModeError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Produce a clear, user-facing error for modes that are reserved but
|
|
12
|
+
* not yet implemented. Called from the CLI before attempting to start
|
|
13
|
+
* the server in an unsupported mode. We explicitly link the tracking
|
|
14
|
+
* issue so users have a concrete next step.
|
|
15
|
+
*/
|
|
16
|
+
export declare function notYetImplemented(mode: SverkloMode): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Runtime mode selection (issue #12).
|
|
2
|
+
//
|
|
3
|
+
// Sverklo is local-first by default and intentionally so. But a shared /
|
|
4
|
+
// team / cloud story is a natural monetization path for later. To avoid
|
|
5
|
+
// painting ourselves into a corner, we introduce the mode concept now
|
|
6
|
+
// as a single seam the rest of the code can branch on.
|
|
7
|
+
//
|
|
8
|
+
// Three modes:
|
|
9
|
+
//
|
|
10
|
+
// embedded (default)
|
|
11
|
+
// Current behavior. Indexes live in ~/.sverklo/<project>/, one
|
|
12
|
+
// process per project, nothing remote. Zero config. Everything
|
|
13
|
+
// works without opting in.
|
|
14
|
+
//
|
|
15
|
+
// shared (reserved)
|
|
16
|
+
// A local daemon process holds indexes for multiple projects and
|
|
17
|
+
// multiple MCP server invocations connect to it. Useful for teams
|
|
18
|
+
// running multiple checkouts on the same laptop, or for the
|
|
19
|
+
// eventual "team cache" feature. NOT YET IMPLEMENTED.
|
|
20
|
+
//
|
|
21
|
+
// cloud (reserved)
|
|
22
|
+
// A hosted sverklo endpoint running the same tool set against
|
|
23
|
+
// centrally-indexed repos. For the eventual Sverklo Team tier.
|
|
24
|
+
// NOT YET IMPLEMENTED.
|
|
25
|
+
//
|
|
26
|
+
// The mode resolver below is the single place where the CLI flag /
|
|
27
|
+
// env var is parsed. If you need mode-specific behavior, call
|
|
28
|
+
// getMode() and branch on the return value — don't re-parse.
|
|
29
|
+
//
|
|
30
|
+
// Hard-failure rule (inspired by Qdrant MCP's QDRANT_LOCAL_PATH vs
|
|
31
|
+
// QDRANT_URL split): ambiguous config is an error, never a silent
|
|
32
|
+
// fallback. If a user sets BOTH --mode cloud AND a local path, crash
|
|
33
|
+
// with a clear message so they know to pick one.
|
|
34
|
+
export function resolveMode(argv, env = process.env) {
|
|
35
|
+
// Precedence: CLI flag > env var > default.
|
|
36
|
+
let fromArgv = null;
|
|
37
|
+
for (const arg of argv) {
|
|
38
|
+
if (arg.startsWith("--mode=")) {
|
|
39
|
+
const val = arg.split("=")[1];
|
|
40
|
+
if (isSverkloMode(val))
|
|
41
|
+
fromArgv = val;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (fromArgv) {
|
|
45
|
+
return { mode: fromArgv, reason: `--mode=${fromArgv}` };
|
|
46
|
+
}
|
|
47
|
+
const fromEnv = env.SVERKLO_MODE;
|
|
48
|
+
if (fromEnv && isSverkloMode(fromEnv)) {
|
|
49
|
+
return { mode: fromEnv, reason: `SVERKLO_MODE=${fromEnv}` };
|
|
50
|
+
}
|
|
51
|
+
if (fromEnv && !isSverkloMode(fromEnv)) {
|
|
52
|
+
throw new SverkloModeError(`Invalid SVERKLO_MODE=${fromEnv}. Must be one of: embedded, shared, cloud.`);
|
|
53
|
+
}
|
|
54
|
+
return { mode: "embedded", reason: "default" };
|
|
55
|
+
}
|
|
56
|
+
export class SverkloModeError extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "SverkloModeError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function isSverkloMode(value) {
|
|
63
|
+
return value === "embedded" || value === "shared" || value === "cloud";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Produce a clear, user-facing error for modes that are reserved but
|
|
67
|
+
* not yet implemented. Called from the CLI before attempting to start
|
|
68
|
+
* the server in an unsupported mode. We explicitly link the tracking
|
|
69
|
+
* issue so users have a concrete next step.
|
|
70
|
+
*/
|
|
71
|
+
export function notYetImplemented(mode) {
|
|
72
|
+
if (mode === "shared") {
|
|
73
|
+
return [
|
|
74
|
+
"Sverklo 'shared' mode is reserved for a future release and not yet implemented.",
|
|
75
|
+
"",
|
|
76
|
+
"Shared mode will let a local sverklo daemon hold indexes for multiple",
|
|
77
|
+
"projects and serve multiple MCP server invocations. Tracked in:",
|
|
78
|
+
"",
|
|
79
|
+
" https://github.com/sverklo/sverklo/issues/12",
|
|
80
|
+
"",
|
|
81
|
+
"For now, run each project with the default 'embedded' mode:",
|
|
82
|
+
"",
|
|
83
|
+
" sverklo /path/to/project",
|
|
84
|
+
"",
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
if (mode === "cloud") {
|
|
88
|
+
return [
|
|
89
|
+
"Sverklo 'cloud' mode is reserved for the eventual Sverklo Team tier",
|
|
90
|
+
"and not yet implemented.",
|
|
91
|
+
"",
|
|
92
|
+
"Cloud mode will talk to a hosted sverklo endpoint running the same",
|
|
93
|
+
"tool set against centrally-indexed repos. Tracked in:",
|
|
94
|
+
"",
|
|
95
|
+
" https://github.com/sverklo/sverklo/issues/12",
|
|
96
|
+
"",
|
|
97
|
+
"For now, use 'embedded' mode (the default) — everything stays local",
|
|
98
|
+
"and private.",
|
|
99
|
+
"",
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=modes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modes.js","sourceRoot":"","sources":["../../src/modes.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,yEAAyE;AACzE,wEAAwE;AACxE,sEAAsE;AACtE,uDAAuD;AACvD,EAAE;AACF,eAAe;AACf,EAAE;AACF,uBAAuB;AACvB,mEAAmE;AACnE,mEAAmE;AACnE,+BAA+B;AAC/B,EAAE;AACF,sBAAsB;AACtB,qEAAqE;AACrE,sEAAsE;AACtE,gEAAgE;AAChE,0DAA0D;AAC1D,EAAE;AACF,qBAAqB;AACrB,kEAAkE;AAClE,mEAAmE;AACnE,2BAA2B;AAC3B,EAAE;AACF,mEAAmE;AACnE,8DAA8D;AAC9D,6DAA6D;AAC7D,EAAE;AACF,mEAAmE;AACnE,kEAAkE;AAClE,qEAAqE;AACrE,iDAAiD;AASjD,MAAM,UAAU,WAAW,CACzB,IAAc,EACd,MAAyB,OAAO,CAAC,GAAG;IAEpC,4CAA4C;IAC5C,IAAI,QAAQ,GAAuB,IAAI,CAAC;IACxC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,aAAa,CAAC,GAAG,CAAC;gBAAE,QAAQ,GAAG,GAAG,CAAC;QACzC,CAAC;IACH,CAAC;IACD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,QAAQ,EAAE,EAAE,CAAC;IAC1D,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC;IACjC,IAAI,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,OAAO,EAAE,EAAE,CAAC;IAC9D,CAAC;IACD,IAAI,OAAO,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,gBAAgB,CACxB,wBAAwB,OAAO,4CAA4C,CAC5E,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,SAAS,aAAa,CAAC,KAAyB;IAC9C,OAAO,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,OAAO,CAAC;AACzE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAiB;IACjD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO;YACL,iFAAiF;YACjF,EAAE;YACF,uEAAuE;YACvE,iEAAiE;YACjE,EAAE;YACF,gDAAgD;YAChD,EAAE;YACF,6DAA6D;YAC7D,EAAE;YACF,4BAA4B;YAC5B,EAAE;SACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;IACD,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,OAAO;YACL,qEAAqE;YACrE,0BAA0B;YAC1B,EAAE;YACF,oEAAoE;YACpE,uDAAuD;YACvD,EAAE;YACF,gDAAgD;YAChD,EAAE;YACF,qEAAqE;YACrE,cAAc;YACd,EAAE;SACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveMode, notYetImplemented, SverkloModeError } from "./modes.js";
|
|
3
|
+
describe("resolveMode", () => {
|
|
4
|
+
it("defaults to embedded when nothing is set", () => {
|
|
5
|
+
const result = resolveMode([], {});
|
|
6
|
+
expect(result.mode).toBe("embedded");
|
|
7
|
+
expect(result.reason).toBe("default");
|
|
8
|
+
});
|
|
9
|
+
it("reads --mode=embedded from argv", () => {
|
|
10
|
+
const result = resolveMode(["--mode=embedded"], {});
|
|
11
|
+
expect(result.mode).toBe("embedded");
|
|
12
|
+
expect(result.reason).toContain("embedded");
|
|
13
|
+
});
|
|
14
|
+
it("reads SVERKLO_MODE from env", () => {
|
|
15
|
+
const result = resolveMode([], { SVERKLO_MODE: "shared" });
|
|
16
|
+
expect(result.mode).toBe("shared");
|
|
17
|
+
expect(result.reason).toContain("shared");
|
|
18
|
+
});
|
|
19
|
+
it("argv takes precedence over env", () => {
|
|
20
|
+
const result = resolveMode(["--mode=cloud"], { SVERKLO_MODE: "embedded" });
|
|
21
|
+
expect(result.mode).toBe("cloud");
|
|
22
|
+
});
|
|
23
|
+
it("throws on invalid SVERKLO_MODE value", () => {
|
|
24
|
+
expect(() => resolveMode([], { SVERKLO_MODE: "lol" })).toThrow(SverkloModeError);
|
|
25
|
+
});
|
|
26
|
+
it("ignores invalid --mode= value and falls back to default", () => {
|
|
27
|
+
// We don't throw on invalid CLI arg because the user might have a
|
|
28
|
+
// typo and we want to fail loud only via env (which is harder to
|
|
29
|
+
// typo-correct). Behavior: invalid CLI flag is silently ignored,
|
|
30
|
+
// env takes over, default applies.
|
|
31
|
+
const result = resolveMode(["--mode=bogus"], {});
|
|
32
|
+
expect(result.mode).toBe("embedded");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("notYetImplemented", () => {
|
|
36
|
+
it("returns a non-empty message for shared", () => {
|
|
37
|
+
expect(notYetImplemented("shared")).toContain("not yet implemented");
|
|
38
|
+
expect(notYetImplemented("shared")).toContain("issues/12");
|
|
39
|
+
});
|
|
40
|
+
it("returns a non-empty message for cloud", () => {
|
|
41
|
+
expect(notYetImplemented("cloud")).toContain("not yet implemented");
|
|
42
|
+
expect(notYetImplemented("cloud")).toContain("issues/12");
|
|
43
|
+
});
|
|
44
|
+
it("returns empty string for embedded (which is implemented)", () => {
|
|
45
|
+
expect(notYetImplemented("embedded")).toBe("");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
//# sourceMappingURL=modes.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modes.test.js","sourceRoot":"","sources":["../../src/modes.test.ts"],"names":[],"mappings":"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"}
|