@mneme-ai/core 0.22.2 → 0.24.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/htc/abstract.d.ts +41 -0
- package/dist/htc/abstract.d.ts.map +1 -0
- package/dist/htc/abstract.js +97 -0
- package/dist/htc/abstract.js.map +1 -0
- package/dist/htc/abstract.test.d.ts +2 -0
- package/dist/htc/abstract.test.d.ts.map +1 -0
- package/dist/htc/abstract.test.js +169 -0
- package/dist/htc/abstract.test.js.map +1 -0
- package/dist/htc/clusters.d.ts +38 -0
- package/dist/htc/clusters.d.ts.map +1 -0
- package/dist/htc/clusters.js +103 -0
- package/dist/htc/clusters.js.map +1 -0
- package/dist/htc/clusters.test.d.ts +2 -0
- package/dist/htc/clusters.test.d.ts.map +1 -0
- package/dist/htc/clusters.test.js +128 -0
- package/dist/htc/clusters.test.js.map +1 -0
- package/dist/htc/index.d.ts +16 -0
- package/dist/htc/index.d.ts.map +1 -0
- package/dist/htc/index.js +16 -0
- package/dist/htc/index.js.map +1 -0
- package/dist/htc/memoir.d.ts +27 -0
- package/dist/htc/memoir.d.ts.map +1 -0
- package/dist/htc/memoir.js +50 -0
- package/dist/htc/memoir.js.map +1 -0
- package/dist/htc/memoir.test.d.ts +2 -0
- package/dist/htc/memoir.test.d.ts.map +1 -0
- package/dist/htc/memoir.test.js +87 -0
- package/dist/htc/memoir.test.js.map +1 -0
- package/dist/htc/storage.d.ts +24 -0
- package/dist/htc/storage.d.ts.map +1 -0
- package/dist/htc/storage.js +170 -0
- package/dist/htc/storage.js.map +1 -0
- package/dist/htc/storage.test.d.ts +2 -0
- package/dist/htc/storage.test.d.ts.map +1 -0
- package/dist/htc/storage.test.js +327 -0
- package/dist/htc/storage.test.js.map +1 -0
- package/dist/htc/types.d.ts +86 -0
- package/dist/htc/types.d.ts.map +1 -0
- package/dist/htc/types.js +22 -0
- package/dist/htc/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/retrieve/ddtree.d.ts +67 -0
- package/dist/retrieve/ddtree.d.ts.map +1 -0
- package/dist/retrieve/ddtree.js +156 -0
- package/dist/retrieve/ddtree.js.map +1 -0
- package/dist/retrieve/ddtree.test.d.ts +2 -0
- package/dist/retrieve/ddtree.test.d.ts.map +1 -0
- package/dist/retrieve/ddtree.test.js +181 -0
- package/dist/retrieve/ddtree.test.js.map +1 -0
- package/dist/retrieve/index.d.ts +3 -0
- package/dist/retrieve/index.d.ts.map +1 -1
- package/dist/retrieve/index.js +3 -0
- package/dist/retrieve/index.js.map +1 -1
- package/dist/retrieve/leviathan.d.ts +68 -0
- package/dist/retrieve/leviathan.d.ts.map +1 -0
- package/dist/retrieve/leviathan.js +192 -0
- package/dist/retrieve/leviathan.js.map +1 -0
- package/dist/retrieve/leviathan.test.d.ts +2 -0
- package/dist/retrieve/leviathan.test.d.ts.map +1 -0
- package/dist/retrieve/leviathan.test.js +124 -0
- package/dist/retrieve/leviathan.test.js.map +1 -0
- package/dist/retrieve/search.d.ts +7 -0
- package/dist/retrieve/search.d.ts.map +1 -1
- package/dist/retrieve/search.js +31 -3
- package/dist/retrieve/search.js.map +1 -1
- package/dist/retrieve/stream.d.ts +93 -0
- package/dist/retrieve/stream.d.ts.map +1 -0
- package/dist/retrieve/stream.js +52 -0
- package/dist/retrieve/stream.js.map +1 -0
- package/dist/retrieve/stream.test.d.ts +2 -0
- package/dist/retrieve/stream.test.d.ts.map +1 -0
- package/dist/retrieve/stream.test.js +118 -0
- package/dist/retrieve/stream.test.js.map +1 -0
- package/dist/retrieve/synthesize.d.ts +7 -0
- package/dist/retrieve/synthesize.d.ts.map +1 -1
- package/dist/retrieve/synthesize.js +48 -2
- package/dist/retrieve/synthesize.js.map +1 -1
- package/dist/store/schema.d.ts +3 -2
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +42 -1
- package/dist/store/schema.js.map +1 -1
- package/dist/store/sqlite.test.js +1 -1
- package/dist/util/constraint-pruner.d.ts +76 -0
- package/dist/util/constraint-pruner.d.ts.map +1 -0
- package/dist/util/constraint-pruner.js +89 -0
- package/dist/util/constraint-pruner.js.map +1 -0
- package/dist/util/constraint-pruner.test.d.ts +2 -0
- package/dist/util/constraint-pruner.test.d.ts.map +1 -0
- package/dist/util/constraint-pruner.test.js +101 -0
- package/dist/util/constraint-pruner.test.js.map +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.d.ts.map +1 -1
- package/dist/util/index.js +1 -0
- package/dist/util/index.js.map +1 -1
- package/dist/wisdom/index.d.ts +2 -0
- package/dist/wisdom/index.d.ts.map +1 -1
- package/dist/wisdom/index.js +2 -0
- package/dist/wisdom/index.js.map +1 -1
- package/dist/wisdom/mutant-adapt.d.ts +69 -0
- package/dist/wisdom/mutant-adapt.d.ts.map +1 -0
- package/dist/wisdom/mutant-adapt.js +216 -0
- package/dist/wisdom/mutant-adapt.js.map +1 -0
- package/dist/wisdom/mutant-adapt.test.d.ts +2 -0
- package/dist/wisdom/mutant-adapt.test.d.ts.map +1 -0
- package/dist/wisdom/mutant-adapt.test.js +184 -0
- package/dist/wisdom/mutant-adapt.test.js.map +1 -0
- package/dist/wisdom/session.d.ts +60 -0
- package/dist/wisdom/session.d.ts.map +1 -0
- package/dist/wisdom/session.js +193 -0
- package/dist/wisdom/session.js.map +1 -0
- package/dist/wisdom/session.test.d.ts +2 -0
- package/dist/wisdom/session.test.d.ts.map +1 -0
- package/dist/wisdom/session.test.js +161 -0
- package/dist/wisdom/session.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTC Layer 1 — semantic abstracts.
|
|
3
|
+
*
|
|
4
|
+
* For each commit, ask the enricher to condense subject+body+files into ~30
|
|
5
|
+
* tokens of plain-English meaning. The result is cached forever (token cost
|
|
6
|
+
* paid once); subsequent queries reuse it without paying the LLM tax again.
|
|
7
|
+
*
|
|
8
|
+
* Prompt format is INTENTIONALLY rigid — consistency at the abstract layer
|
|
9
|
+
* keeps Layer 2 (cluster summaries) and Layer 3 (memoir) stable too.
|
|
10
|
+
*/
|
|
11
|
+
import type { AbstractResult, HtcEnricher } from "./types.js";
|
|
12
|
+
/** System prompt — kept short + directive, no preamble allowed. */
|
|
13
|
+
export declare const ABSTRACT_SYSTEM_PROMPT: string;
|
|
14
|
+
export interface AbstractCommitInput {
|
|
15
|
+
hash: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
body?: string;
|
|
18
|
+
files?: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface AbstractInput {
|
|
21
|
+
commit: AbstractCommitInput;
|
|
22
|
+
enricher: HtcEnricher;
|
|
23
|
+
}
|
|
24
|
+
export declare function buildAbstractUserPrompt(commit: AbstractCommitInput): string;
|
|
25
|
+
/** Generate a single ~30-token abstract for one commit. */
|
|
26
|
+
export declare function generateAbstract(input: AbstractInput): Promise<AbstractResult>;
|
|
27
|
+
export interface BatchAbstractOptions {
|
|
28
|
+
/** Concurrent in-flight enricher calls; default 3 (free providers like Groq rate-limit). */
|
|
29
|
+
concurrency?: number;
|
|
30
|
+
onProgress?: (done: number, total: number) => void;
|
|
31
|
+
onError?: (hash: string, err: string) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate abstracts for many commits with a concurrency cap. Failures are
|
|
35
|
+
* recorded via onError but DO NOT abort the batch — the caller decides
|
|
36
|
+
* whether to retry the failed hashes.
|
|
37
|
+
*
|
|
38
|
+
* Returns one AbstractResult per SUCCESSFUL commit (failures omitted).
|
|
39
|
+
*/
|
|
40
|
+
export declare function generateAbstractsBatch(commits: AbstractCommitInput[], enricher: HtcEnricher, opts?: BatchAbstractOptions): Promise<AbstractResult[]>;
|
|
41
|
+
//# sourceMappingURL=abstract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abstract.d.ts","sourceRoot":"","sources":["../../src/htc/abstract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9D,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,QAEuC,CAAC;AAE3E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,QAAQ,EAAE,WAAW,CAAC;CACvB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAe3E;AAED,2DAA2D;AAC3D,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAwBpF;AAED,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,mBAAmB,EAAE,EAC9B,QAAQ,EAAE,WAAW,EACrB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,cAAc,EAAE,CAAC,CA6B3B"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { estimateTokens } from "./types.js";
|
|
2
|
+
/** System prompt — kept short + directive, no preamble allowed. */
|
|
3
|
+
export const ABSTRACT_SYSTEM_PROMPT = "You are a senior engineer condensing git commit data into ~30 tokens " +
|
|
4
|
+
"of plain-English meaning. Output ONLY the condensed text, no preamble.";
|
|
5
|
+
export function buildAbstractUserPrompt(commit) {
|
|
6
|
+
const subject = commit.subject?.trim() || "(empty)";
|
|
7
|
+
const body = commit.body?.trim() ? commit.body.trim() : "(none)";
|
|
8
|
+
const files = (commit.files ?? []).slice(0, 3);
|
|
9
|
+
const filesLine = files.length ? files.join(", ") : "(none)";
|
|
10
|
+
return [
|
|
11
|
+
`Commit subject: ${subject}`,
|
|
12
|
+
`Body: ${body}`,
|
|
13
|
+
`Files (sample): ${filesLine}`,
|
|
14
|
+
"",
|
|
15
|
+
"Condense to ~30 tokens. Format: \"WHAT changed + WHY\". Examples:",
|
|
16
|
+
" \"auth: replaced session cookies with JWT for stateless CDN deploys\"",
|
|
17
|
+
" \"fix: AR property tap shows detail on Android — race in event listener\"",
|
|
18
|
+
" \"refactor: split payment.ts into 3 modules so legacy + V2 can coexist\"",
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
/** Generate a single ~30-token abstract for one commit. */
|
|
22
|
+
export async function generateAbstract(input) {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
const userPrompt = buildAbstractUserPrompt(input.commit);
|
|
25
|
+
const result = await input.enricher.enrich({
|
|
26
|
+
system: ABSTRACT_SYSTEM_PROMPT,
|
|
27
|
+
user: userPrompt,
|
|
28
|
+
// Cap output so the model can't blow past ~30 tokens; 80 = comfortable
|
|
29
|
+
// headroom for sub-word splits without inviting paragraphs.
|
|
30
|
+
maxTokens: 80,
|
|
31
|
+
// Low temp — we want stable, comparable abstracts across runs.
|
|
32
|
+
temperature: 0.2,
|
|
33
|
+
});
|
|
34
|
+
const generationMs = Date.now() - start;
|
|
35
|
+
const text = (result.text ?? "").trim();
|
|
36
|
+
// Strip wrapping quotes the model sometimes adds.
|
|
37
|
+
const abstract = stripWrappingQuotes(text);
|
|
38
|
+
return {
|
|
39
|
+
hash: input.commit.hash,
|
|
40
|
+
abstract,
|
|
41
|
+
tokenCount: estimateTokens(abstract),
|
|
42
|
+
generationMs,
|
|
43
|
+
generator: input.enricher.name,
|
|
44
|
+
generatedAt: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate abstracts for many commits with a concurrency cap. Failures are
|
|
49
|
+
* recorded via onError but DO NOT abort the batch — the caller decides
|
|
50
|
+
* whether to retry the failed hashes.
|
|
51
|
+
*
|
|
52
|
+
* Returns one AbstractResult per SUCCESSFUL commit (failures omitted).
|
|
53
|
+
*/
|
|
54
|
+
export async function generateAbstractsBatch(commits, enricher, opts = {}) {
|
|
55
|
+
const concurrency = Math.max(1, opts.concurrency ?? 3);
|
|
56
|
+
const results = [];
|
|
57
|
+
let done = 0;
|
|
58
|
+
// Simple worker-pool: N workers pulling from a shared cursor.
|
|
59
|
+
let cursor = 0;
|
|
60
|
+
async function worker() {
|
|
61
|
+
while (true) {
|
|
62
|
+
const i = cursor++;
|
|
63
|
+
if (i >= commits.length)
|
|
64
|
+
return;
|
|
65
|
+
const c = commits[i];
|
|
66
|
+
try {
|
|
67
|
+
const r = await generateAbstract({ commit: c, enricher });
|
|
68
|
+
results.push(r);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const msg = err?.message ?? String(err);
|
|
72
|
+
opts.onError?.(c.hash, msg);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
done++;
|
|
76
|
+
opts.onProgress?.(done, commits.length);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const workers = [];
|
|
81
|
+
for (let i = 0; i < concurrency; i++)
|
|
82
|
+
workers.push(worker());
|
|
83
|
+
await Promise.all(workers);
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
function stripWrappingQuotes(s) {
|
|
87
|
+
if (s.length < 2)
|
|
88
|
+
return s;
|
|
89
|
+
const first = s.charCodeAt(0);
|
|
90
|
+
const last = s.charCodeAt(s.length - 1);
|
|
91
|
+
// ", ', or curly quotes
|
|
92
|
+
const QUOTES = [0x22, 0x27, 0x201c, 0x201d, 0x2018, 0x2019];
|
|
93
|
+
if (QUOTES.includes(first) && QUOTES.includes(last))
|
|
94
|
+
return s.slice(1, -1).trim();
|
|
95
|
+
return s;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=abstract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abstract.js","sourceRoot":"","sources":["../../src/htc/abstract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abstract.test.d.ts","sourceRoot":"","sources":["../../src/htc/abstract.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { ABSTRACT_SYSTEM_PROMPT, buildAbstractUserPrompt, generateAbstract, generateAbstractsBatch, } from "./abstract.js";
|
|
3
|
+
import { estimateTokens } from "./types.js";
|
|
4
|
+
function mockEnricher(text, name = "mock:test") {
|
|
5
|
+
return {
|
|
6
|
+
name,
|
|
7
|
+
enrich: vi.fn().mockResolvedValue({ text }),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
describe("buildAbstractUserPrompt", () => {
|
|
11
|
+
it("includes subject + body + first 3 files", () => {
|
|
12
|
+
const prompt = buildAbstractUserPrompt({
|
|
13
|
+
hash: "a".repeat(40),
|
|
14
|
+
subject: "fix: race in event listener",
|
|
15
|
+
body: "Android only — touch event fired before render flush.",
|
|
16
|
+
files: ["app/AR.tsx", "app/utils/events.ts", "app/test.ts", "app/extra.ts"],
|
|
17
|
+
});
|
|
18
|
+
expect(prompt).toContain("fix: race in event listener");
|
|
19
|
+
expect(prompt).toContain("Android only");
|
|
20
|
+
expect(prompt).toContain("app/AR.tsx, app/utils/events.ts, app/test.ts");
|
|
21
|
+
// Only first 3 files — extra.ts must be excluded.
|
|
22
|
+
expect(prompt).not.toContain("app/extra.ts");
|
|
23
|
+
});
|
|
24
|
+
it("falls back to '(empty)' / '(none)' when fields missing", () => {
|
|
25
|
+
const prompt = buildAbstractUserPrompt({ hash: "x", subject: "" });
|
|
26
|
+
expect(prompt).toContain("Commit subject: (empty)");
|
|
27
|
+
expect(prompt).toContain("Body: (none)");
|
|
28
|
+
expect(prompt).toContain("Files (sample): (none)");
|
|
29
|
+
});
|
|
30
|
+
it("includes the rigid format examples (consistency anchor)", () => {
|
|
31
|
+
const prompt = buildAbstractUserPrompt({ hash: "x", subject: "anything" });
|
|
32
|
+
expect(prompt).toContain("WHAT changed + WHY");
|
|
33
|
+
expect(prompt).toContain("auth: replaced session cookies with JWT");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("generateAbstract", () => {
|
|
37
|
+
it("calls enricher with system + user prompts and returns AbstractResult", async () => {
|
|
38
|
+
const enricher = mockEnricher("auth: replaced session cookies with JWT for stateless CDN deploys", "ollama:qwen2.5:3b");
|
|
39
|
+
const r = await generateAbstract({
|
|
40
|
+
commit: {
|
|
41
|
+
hash: "a".repeat(40),
|
|
42
|
+
subject: "auth: switch to JWT",
|
|
43
|
+
body: "Sessions don't replicate across CDN.",
|
|
44
|
+
files: ["src/auth.ts"],
|
|
45
|
+
},
|
|
46
|
+
enricher,
|
|
47
|
+
});
|
|
48
|
+
expect(r.hash).toBe("a".repeat(40));
|
|
49
|
+
expect(r.abstract).toContain("JWT");
|
|
50
|
+
expect(r.generator).toBe("ollama:qwen2.5:3b");
|
|
51
|
+
expect(r.tokenCount).toBeGreaterThan(0);
|
|
52
|
+
expect(r.generationMs).toBeGreaterThanOrEqual(0);
|
|
53
|
+
expect(r.generatedAt).toBeDefined();
|
|
54
|
+
expect(enricher.enrich).toHaveBeenCalledTimes(1);
|
|
55
|
+
const call = enricher.enrich.mock.calls[0][0];
|
|
56
|
+
expect(call.system).toBe(ABSTRACT_SYSTEM_PROMPT);
|
|
57
|
+
expect(call.user).toContain("auth: switch to JWT");
|
|
58
|
+
expect(call.maxTokens).toBe(80);
|
|
59
|
+
});
|
|
60
|
+
it("token count math = ceil(words * 1.3)", () => {
|
|
61
|
+
// 10 words → ceil(10 * 1.3) = 13
|
|
62
|
+
expect(estimateTokens("one two three four five six seven eight nine ten")).toBe(13);
|
|
63
|
+
expect(estimateTokens("")).toBe(0);
|
|
64
|
+
expect(estimateTokens(" ")).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
it("strips wrapping straight + curly quotes from model output", async () => {
|
|
67
|
+
const enricher = mockEnricher('"refactor: split payment.ts into 3 modules"');
|
|
68
|
+
const r = await generateAbstract({
|
|
69
|
+
commit: { hash: "h", subject: "refactor" },
|
|
70
|
+
enricher,
|
|
71
|
+
});
|
|
72
|
+
expect(r.abstract.startsWith('"')).toBe(false);
|
|
73
|
+
expect(r.abstract.endsWith('"')).toBe(false);
|
|
74
|
+
expect(r.abstract).toBe("refactor: split payment.ts into 3 modules");
|
|
75
|
+
const enricher2 = mockEnricher("“feat: X”");
|
|
76
|
+
const r2 = await generateAbstract({
|
|
77
|
+
commit: { hash: "h", subject: "feat" },
|
|
78
|
+
enricher: enricher2,
|
|
79
|
+
});
|
|
80
|
+
expect(r2.abstract).toBe("feat: X");
|
|
81
|
+
});
|
|
82
|
+
it("trims whitespace around the model's text", async () => {
|
|
83
|
+
const enricher = mockEnricher(" feat: trim me \n\n");
|
|
84
|
+
const r = await generateAbstract({
|
|
85
|
+
commit: { hash: "h", subject: "x" },
|
|
86
|
+
enricher,
|
|
87
|
+
});
|
|
88
|
+
expect(r.abstract).toBe("feat: trim me");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("generateAbstractsBatch", () => {
|
|
92
|
+
it("processes every commit and returns one result each", async () => {
|
|
93
|
+
const enricher = mockEnricher("abstract text");
|
|
94
|
+
const commits = Array.from({ length: 5 }, (_, i) => ({
|
|
95
|
+
hash: String(i).padStart(40, "0"),
|
|
96
|
+
subject: `subject ${i}`,
|
|
97
|
+
}));
|
|
98
|
+
const out = await generateAbstractsBatch(commits, enricher, { concurrency: 2 });
|
|
99
|
+
expect(out).toHaveLength(5);
|
|
100
|
+
expect(enricher.enrich).toHaveBeenCalledTimes(5);
|
|
101
|
+
});
|
|
102
|
+
it("emits onProgress for every completion", async () => {
|
|
103
|
+
const enricher = mockEnricher("ok");
|
|
104
|
+
const onProgress = vi.fn();
|
|
105
|
+
const commits = [
|
|
106
|
+
{ hash: "a".repeat(40), subject: "1" },
|
|
107
|
+
{ hash: "b".repeat(40), subject: "2" },
|
|
108
|
+
{ hash: "c".repeat(40), subject: "3" },
|
|
109
|
+
];
|
|
110
|
+
await generateAbstractsBatch(commits, enricher, { concurrency: 2, onProgress });
|
|
111
|
+
expect(onProgress).toHaveBeenCalledTimes(3);
|
|
112
|
+
// Last call should have done === total.
|
|
113
|
+
const last = onProgress.mock.calls[onProgress.mock.calls.length - 1];
|
|
114
|
+
expect(last[0]).toBe(3);
|
|
115
|
+
expect(last[1]).toBe(3);
|
|
116
|
+
});
|
|
117
|
+
it("records errors via onError without aborting the batch", async () => {
|
|
118
|
+
const enricher = {
|
|
119
|
+
name: "mock:flaky",
|
|
120
|
+
enrich: vi.fn().mockImplementation((input) => {
|
|
121
|
+
if (input.user.includes("FAIL")) {
|
|
122
|
+
return Promise.reject(new Error("rate-limit hit"));
|
|
123
|
+
}
|
|
124
|
+
return Promise.resolve({ text: "ok" });
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
const onError = vi.fn();
|
|
128
|
+
const commits = [
|
|
129
|
+
{ hash: "a".repeat(40), subject: "good 1" },
|
|
130
|
+
{ hash: "b".repeat(40), subject: "FAIL me" },
|
|
131
|
+
{ hash: "c".repeat(40), subject: "good 2" },
|
|
132
|
+
];
|
|
133
|
+
const out = await generateAbstractsBatch(commits, enricher, {
|
|
134
|
+
concurrency: 1,
|
|
135
|
+
onError,
|
|
136
|
+
});
|
|
137
|
+
expect(out).toHaveLength(2); // failures excluded
|
|
138
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
139
|
+
expect(onError.mock.calls[0][0]).toBe("b".repeat(40));
|
|
140
|
+
expect(onError.mock.calls[0][1]).toContain("rate-limit");
|
|
141
|
+
});
|
|
142
|
+
it("respects concurrency cap (≤ N in flight)", async () => {
|
|
143
|
+
let inFlight = 0;
|
|
144
|
+
let peak = 0;
|
|
145
|
+
const enricher = {
|
|
146
|
+
name: "mock:concurrent",
|
|
147
|
+
enrich: vi.fn().mockImplementation(async () => {
|
|
148
|
+
inFlight++;
|
|
149
|
+
peak = Math.max(peak, inFlight);
|
|
150
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
151
|
+
inFlight--;
|
|
152
|
+
return { text: "ok" };
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
const commits = Array.from({ length: 10 }, (_, i) => ({
|
|
156
|
+
hash: String(i).padStart(40, "0"),
|
|
157
|
+
subject: `s${i}`,
|
|
158
|
+
}));
|
|
159
|
+
await generateAbstractsBatch(commits, enricher, { concurrency: 3 });
|
|
160
|
+
expect(peak).toBeLessThanOrEqual(3);
|
|
161
|
+
expect(peak).toBeGreaterThan(0);
|
|
162
|
+
});
|
|
163
|
+
it("defaults concurrency to 3 when not specified", async () => {
|
|
164
|
+
const enricher = mockEnricher("ok");
|
|
165
|
+
const out = await generateAbstractsBatch([{ hash: "a".repeat(40), subject: "x" }], enricher);
|
|
166
|
+
expect(out).toHaveLength(1);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
//# sourceMappingURL=abstract.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abstract.test.js","sourceRoot":"","sources":["../../src/htc/abstract.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTC Layer 2 — topic cluster summaries.
|
|
3
|
+
*
|
|
4
|
+
* Takes pre-built clusters (from insights/cluster.buildClusters) plus the
|
|
5
|
+
* cached Layer-1 abstracts and asks the enricher to roll each cluster up
|
|
6
|
+
* into a ~100-token paragraph: topic + major changes + sequence.
|
|
7
|
+
*
|
|
8
|
+
* The cluster's `label` is extracted from the LLM's output (first sentence
|
|
9
|
+
* heuristic). We never invent labels — if the model produces nothing
|
|
10
|
+
* usable, we fall back to "cluster <id>".
|
|
11
|
+
*/
|
|
12
|
+
import type { ClusterSummary, HtcEnricher } from "./types.js";
|
|
13
|
+
export declare const CLUSTER_SYSTEM_PROMPT: string;
|
|
14
|
+
export interface ClusterInput {
|
|
15
|
+
/** Stable cluster identifier; persisted as cluster_id. */
|
|
16
|
+
id: string;
|
|
17
|
+
memberHashes: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface GenerateClusterSummariesOptions {
|
|
20
|
+
concurrency?: number;
|
|
21
|
+
onProgress?: (done: number, total: number) => void;
|
|
22
|
+
onError?: (clusterId: string, err: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare function buildClusterUserPrompt(abstracts: string[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Extract a short topic label from the LLM's summary. Heuristic: take the
|
|
27
|
+
* first colon-prefixed phrase ("auth: …") OR the first 4 words of the first
|
|
28
|
+
* sentence. Caps at ~40 chars. Never invents — falls back to caller-provided
|
|
29
|
+
* fallback when the summary is empty.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractLabel(summary: string, fallback: string): string;
|
|
32
|
+
export declare function generateClusterSummary(cluster: ClusterInput, abstracts: Map<string, string>, enricher: HtcEnricher): Promise<ClusterSummary>;
|
|
33
|
+
/**
|
|
34
|
+
* Batch summary generation with bounded concurrency. Failures recorded via
|
|
35
|
+
* onError; the surviving results are returned.
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateClusterSummaries(abstracts: Map<string, string>, clusters: ClusterInput[], enricher: HtcEnricher, opts?: GenerateClusterSummariesOptions): Promise<ClusterSummary[]>;
|
|
38
|
+
//# sourceMappingURL=clusters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clusters.d.ts","sourceRoot":"","sources":["../../src/htc/clusters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9D,eAAO,MAAM,qBAAqB,QAGmB,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,+BAA+B;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAOlE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUtE;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,YAAY,EACrB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,cAAc,CAAC,CAoCzB;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,QAAQ,EAAE,YAAY,EAAE,EACxB,QAAQ,EAAE,WAAW,EACrB,IAAI,GAAE,+BAAoC,GACzC,OAAO,CAAC,cAAc,EAAE,CAAC,CA4B3B"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { estimateTokens } from "./types.js";
|
|
2
|
+
export const CLUSTER_SYSTEM_PROMPT = "Summarize a group of related git commits into ~100 tokens. Output a brief " +
|
|
3
|
+
"paragraph that names the topic, lists the major changes, and notes any " +
|
|
4
|
+
"sequence (e.g. \"started with X, evolved to Y\").";
|
|
5
|
+
export function buildClusterUserPrompt(abstracts) {
|
|
6
|
+
const lines = ["Commit abstracts (one per line):"];
|
|
7
|
+
for (const a of abstracts) {
|
|
8
|
+
const trimmed = a.trim();
|
|
9
|
+
if (trimmed)
|
|
10
|
+
lines.push(` - ${trimmed}`);
|
|
11
|
+
}
|
|
12
|
+
return lines.join("\n");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Extract a short topic label from the LLM's summary. Heuristic: take the
|
|
16
|
+
* first colon-prefixed phrase ("auth: …") OR the first 4 words of the first
|
|
17
|
+
* sentence. Caps at ~40 chars. Never invents — falls back to caller-provided
|
|
18
|
+
* fallback when the summary is empty.
|
|
19
|
+
*/
|
|
20
|
+
export function extractLabel(summary, fallback) {
|
|
21
|
+
const trimmed = summary.trim();
|
|
22
|
+
if (!trimmed)
|
|
23
|
+
return fallback;
|
|
24
|
+
// Pattern A: "topic: rest of sentence"
|
|
25
|
+
const colonMatch = trimmed.match(/^([A-Za-z0-9 _\-/.]{2,40}?):/);
|
|
26
|
+
if (colonMatch)
|
|
27
|
+
return colonMatch[1].trim().toLowerCase();
|
|
28
|
+
// Pattern B: first sentence, first 4 words.
|
|
29
|
+
const firstSentence = trimmed.split(/[.!?\n]/)[0].trim();
|
|
30
|
+
const words = firstSentence.split(/\s+/).slice(0, 4).join(" ");
|
|
31
|
+
return (words || fallback).slice(0, 40).toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
export async function generateClusterSummary(cluster, abstracts, enricher) {
|
|
34
|
+
// Pull the abstracts for this cluster's members. Missing ones are skipped
|
|
35
|
+
// (caller should ensure Layer 1 was generated; we don't refuse, but a
|
|
36
|
+
// cluster with zero abstracts can't be summarized usefully).
|
|
37
|
+
const memberAbstracts = [];
|
|
38
|
+
for (const hash of cluster.memberHashes) {
|
|
39
|
+
const a = abstracts.get(hash);
|
|
40
|
+
if (a)
|
|
41
|
+
memberAbstracts.push(a);
|
|
42
|
+
}
|
|
43
|
+
if (memberAbstracts.length === 0) {
|
|
44
|
+
throw new Error(`Cluster ${cluster.id} has no Layer-1 abstracts available; generate abstracts first.`);
|
|
45
|
+
}
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
const result = await enricher.enrich({
|
|
48
|
+
system: CLUSTER_SYSTEM_PROMPT,
|
|
49
|
+
user: buildClusterUserPrompt(memberAbstracts),
|
|
50
|
+
// ~100 tokens target → 200 cap leaves room without inviting essays.
|
|
51
|
+
maxTokens: 200,
|
|
52
|
+
temperature: 0.3,
|
|
53
|
+
});
|
|
54
|
+
const generationMs = Date.now() - start;
|
|
55
|
+
const summary = (result.text ?? "").trim();
|
|
56
|
+
const label = extractLabel(summary, `cluster ${cluster.id}`);
|
|
57
|
+
return {
|
|
58
|
+
clusterId: cluster.id,
|
|
59
|
+
label,
|
|
60
|
+
summary,
|
|
61
|
+
memberHashes: cluster.memberHashes,
|
|
62
|
+
tokenCount: estimateTokens(summary),
|
|
63
|
+
generationMs,
|
|
64
|
+
generator: enricher.name,
|
|
65
|
+
generatedAt: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Batch summary generation with bounded concurrency. Failures recorded via
|
|
70
|
+
* onError; the surviving results are returned.
|
|
71
|
+
*/
|
|
72
|
+
export async function generateClusterSummaries(abstracts, clusters, enricher, opts = {}) {
|
|
73
|
+
const concurrency = Math.max(1, opts.concurrency ?? 3);
|
|
74
|
+
const out = [];
|
|
75
|
+
let done = 0;
|
|
76
|
+
let cursor = 0;
|
|
77
|
+
async function worker() {
|
|
78
|
+
while (true) {
|
|
79
|
+
const i = cursor++;
|
|
80
|
+
if (i >= clusters.length)
|
|
81
|
+
return;
|
|
82
|
+
const c = clusters[i];
|
|
83
|
+
try {
|
|
84
|
+
const s = await generateClusterSummary(c, abstracts, enricher);
|
|
85
|
+
out.push(s);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const msg = err?.message ?? String(err);
|
|
89
|
+
opts.onError?.(c.id, msg);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
done++;
|
|
93
|
+
opts.onProgress?.(done, clusters.length);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const workers = [];
|
|
98
|
+
for (let i = 0; i < concurrency; i++)
|
|
99
|
+
workers.push(worker());
|
|
100
|
+
await Promise.all(workers);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=clusters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clusters.js","sourceRoot":"","sources":["../../src/htc/clusters.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clusters.test.d.ts","sourceRoot":"","sources":["../../src/htc/clusters.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { CLUSTER_SYSTEM_PROMPT, buildClusterUserPrompt, extractLabel, generateClusterSummary, generateClusterSummaries, } from "./clusters.js";
|
|
3
|
+
function mockEnricher(text, name = "mock:test") {
|
|
4
|
+
return {
|
|
5
|
+
name,
|
|
6
|
+
enrich: vi.fn().mockResolvedValue({ text }),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
describe("buildClusterUserPrompt", () => {
|
|
10
|
+
it("formats abstracts as a bulleted list", () => {
|
|
11
|
+
const prompt = buildClusterUserPrompt([
|
|
12
|
+
"auth: replaced session cookies with JWT",
|
|
13
|
+
"auth: rotated JWT signing keys quarterly",
|
|
14
|
+
]);
|
|
15
|
+
expect(prompt).toContain("Commit abstracts (one per line):");
|
|
16
|
+
expect(prompt).toContain(" - auth: replaced session cookies with JWT");
|
|
17
|
+
expect(prompt).toContain(" - auth: rotated JWT signing keys");
|
|
18
|
+
});
|
|
19
|
+
it("skips blank entries", () => {
|
|
20
|
+
const prompt = buildClusterUserPrompt(["real one", " ", "", "another"]);
|
|
21
|
+
const bullets = prompt.split("\n").filter((l) => l.startsWith(" - "));
|
|
22
|
+
expect(bullets).toHaveLength(2);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("extractLabel", () => {
|
|
26
|
+
it("uses the colon-prefixed topic when present", () => {
|
|
27
|
+
expect(extractLabel("auth: switched to JWT for stateless deploys", "fb")).toBe("auth");
|
|
28
|
+
expect(extractLabel("payment refactor: split into V2", "fb")).toBe("payment refactor");
|
|
29
|
+
});
|
|
30
|
+
it("falls back to first 4 words when no colon", () => {
|
|
31
|
+
expect(extractLabel("Payment module evolved over many quarters.", "fb")).toBe("payment module evolved over");
|
|
32
|
+
});
|
|
33
|
+
it("uses fallback on empty input", () => {
|
|
34
|
+
expect(extractLabel("", "cluster 7")).toBe("cluster 7");
|
|
35
|
+
expect(extractLabel(" ", "cluster 7")).toBe("cluster 7");
|
|
36
|
+
});
|
|
37
|
+
it("caps label length at 40 chars", () => {
|
|
38
|
+
const long = "this is a very long opening sentence with no colon punctuation here";
|
|
39
|
+
const label = extractLabel(long, "fb");
|
|
40
|
+
expect(label.length).toBeLessThanOrEqual(40);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe("generateClusterSummary", () => {
|
|
44
|
+
it("calls enricher with cluster prompt + builds summary", async () => {
|
|
45
|
+
const enricher = mockEnricher("auth refactor: started with cookie sessions, evolved to JWT for CDN-friendly deploys.", "groq:llama-3.3-70b");
|
|
46
|
+
const abstracts = new Map([
|
|
47
|
+
["aaa", "auth: cookie session"],
|
|
48
|
+
["bbb", "auth: switched to JWT"],
|
|
49
|
+
]);
|
|
50
|
+
const r = await generateClusterSummary({ id: "c1", memberHashes: ["aaa", "bbb"] }, abstracts, enricher);
|
|
51
|
+
expect(r.clusterId).toBe("c1");
|
|
52
|
+
expect(r.label).toBe("auth refactor");
|
|
53
|
+
expect(r.summary).toContain("JWT");
|
|
54
|
+
expect(r.memberHashes).toEqual(["aaa", "bbb"]);
|
|
55
|
+
expect(r.tokenCount).toBeGreaterThan(0);
|
|
56
|
+
expect(r.generator).toBe("groq:llama-3.3-70b");
|
|
57
|
+
const call = enricher.enrich.mock.calls[0][0];
|
|
58
|
+
expect(call.system).toBe(CLUSTER_SYSTEM_PROMPT);
|
|
59
|
+
expect(call.user).toContain("auth: cookie session");
|
|
60
|
+
expect(call.user).toContain("auth: switched to JWT");
|
|
61
|
+
});
|
|
62
|
+
it("throws when no Layer-1 abstracts exist for the cluster", async () => {
|
|
63
|
+
const enricher = mockEnricher("never called");
|
|
64
|
+
await expect(generateClusterSummary({ id: "c2", memberHashes: ["missing1", "missing2"] }, new Map(), enricher)).rejects.toThrow(/no Layer-1 abstracts/);
|
|
65
|
+
expect(enricher.enrich).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
it("uses fallback label when summary has no clear topic", async () => {
|
|
68
|
+
const enricher = mockEnricher("");
|
|
69
|
+
await expect(generateClusterSummary({ id: "c3", memberHashes: ["aaa"] }, new Map([["aaa", "anything"]]), enricher)).resolves.toMatchObject({ label: "cluster c3" });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("generateClusterSummaries", () => {
|
|
73
|
+
it("processes all clusters and returns ClusterSummary[]", async () => {
|
|
74
|
+
const enricher = mockEnricher("topic: brief summary");
|
|
75
|
+
const abstracts = new Map([
|
|
76
|
+
["a", "abstract a"],
|
|
77
|
+
["b", "abstract b"],
|
|
78
|
+
["c", "abstract c"],
|
|
79
|
+
["d", "abstract d"],
|
|
80
|
+
]);
|
|
81
|
+
const clusters = [
|
|
82
|
+
{ id: "c1", memberHashes: ["a", "b"] },
|
|
83
|
+
{ id: "c2", memberHashes: ["c", "d"] },
|
|
84
|
+
];
|
|
85
|
+
const out = await generateClusterSummaries(abstracts, clusters, enricher, {
|
|
86
|
+
concurrency: 2,
|
|
87
|
+
});
|
|
88
|
+
expect(out).toHaveLength(2);
|
|
89
|
+
expect(enricher.enrich).toHaveBeenCalledTimes(2);
|
|
90
|
+
});
|
|
91
|
+
it("records errors via onError without aborting", async () => {
|
|
92
|
+
const enricher = {
|
|
93
|
+
name: "mock:flaky",
|
|
94
|
+
enrich: vi.fn().mockImplementation((input) => {
|
|
95
|
+
if (input.user.includes("BAD"))
|
|
96
|
+
return Promise.reject(new Error("server 503"));
|
|
97
|
+
return Promise.resolve({ text: "ok: clean summary" });
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
const abstracts = new Map([
|
|
101
|
+
["a", "good one"],
|
|
102
|
+
["b", "BAD one"],
|
|
103
|
+
["c", "good two"],
|
|
104
|
+
]);
|
|
105
|
+
const onError = vi.fn();
|
|
106
|
+
const out = await generateClusterSummaries(abstracts, [
|
|
107
|
+
{ id: "c1", memberHashes: ["a"] },
|
|
108
|
+
{ id: "c2", memberHashes: ["b"] },
|
|
109
|
+
{ id: "c3", memberHashes: ["c"] },
|
|
110
|
+
], enricher, { concurrency: 1, onError });
|
|
111
|
+
expect(out).toHaveLength(2);
|
|
112
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(onError.mock.calls[0][0]).toBe("c2");
|
|
114
|
+
});
|
|
115
|
+
it("invokes onProgress with monotonically increasing done count", async () => {
|
|
116
|
+
const enricher = mockEnricher("topic: x");
|
|
117
|
+
const abstracts = new Map([["a", "x"]]);
|
|
118
|
+
const onProgress = vi.fn();
|
|
119
|
+
await generateClusterSummaries(abstracts, [
|
|
120
|
+
{ id: "c1", memberHashes: ["a"] },
|
|
121
|
+
{ id: "c2", memberHashes: ["a"] },
|
|
122
|
+
], enricher, { onProgress });
|
|
123
|
+
expect(onProgress).toHaveBeenCalledTimes(2);
|
|
124
|
+
const last = onProgress.mock.calls[onProgress.mock.calls.length - 1];
|
|
125
|
+
expect(last).toEqual([2, 2]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=clusters.test.js.map
|