@jeffober/thatch 0.1.28 → 0.1.29
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 +24 -10
- package/artifacts/skills/thatch-code-review.md +9 -7
- package/artifacts/skills/thatch-review-economy.md +97 -0
- package/artifacts/skills/thatch-review-followup.md +1 -1
- package/artifacts/skills/thatch-review-synthesizer.md +15 -5
- package/bin/qa-run +38 -0
- package/bin/thatch +10 -5
- package/docs/dev/README.md +71 -18
- package/docs/dev/features/README.md +33 -0
- package/docs/dev/features/behavior-engine.md +271 -0
- package/docs/dev/features/cicd.md +89 -0
- package/docs/dev/features/cli.md +81 -0
- package/docs/dev/features/compaction-recovery.md +48 -0
- package/docs/dev/features/database.md +244 -0
- package/docs/dev/features/deduplication.md +102 -0
- package/docs/dev/features/extraction.md +253 -0
- package/docs/dev/features/hygiene.md +81 -0
- package/docs/dev/features/memory-store.md +154 -0
- package/docs/dev/features/multi-host.md +228 -0
- package/docs/dev/features/nudge-pipeline.md +282 -0
- package/docs/dev/features/prediction-engine.md +253 -0
- package/docs/dev/features/qa-system.md +205 -0
- package/docs/dev/features/session-lifecycle.md +110 -0
- package/docs/dev/features/setup.md +199 -0
- package/docs/dev/features/sideband.md +104 -0
- package/docs/dev/gotchas.md +3 -3
- package/docs/dev/mcp-parity.md +6 -4
- package/docs/dev/setup-and-hooks.md +4 -4
- package/docs/dev/skills.md +10 -8
- package/docs/plans/README.md +2 -2
- package/docs/user/README.md +58 -20
- package/docs/user/behavior-engine.md +85 -0
- package/docs/user/cli.md +98 -0
- package/docs/user/code-review.md +73 -0
- package/docs/user/deduplication.md +70 -0
- package/docs/user/default-behaviors.md +75 -0
- package/docs/user/extraction.md +74 -0
- package/docs/user/hygiene.md +76 -0
- package/docs/user/memory.md +81 -0
- package/docs/user/prediction-engine.md +87 -0
- package/docs/user/setup.md +110 -0
- package/docs/user/skills.md +97 -0
- package/package.json +2 -2
- package/src/behavior.ts +120 -0
- package/src/db.ts +160 -417
- package/src/embeddings.ts +0 -41
- package/src/extraction.ts +2 -2
- package/src/git.ts +3 -1
- package/src/index.ts +55 -9
- package/src/mcp.ts +30 -1
- package/src/prediction.ts +127 -0
- package/src/prompts.ts +135 -188
- package/src/scoring-engine.ts +374 -0
- package/src/seed-behaviors.ts +339 -0
- package/src/setup.ts +2 -6
- package/src/sideband.ts +36 -8
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +172 -25
- package/src/vector-math.ts +30 -0
- package/docs/qa/README.md +0 -175
package/src/tool-defs.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type { ThatchDB, DedupCandidate } from "./db";
|
|
2
|
+
import type { ThatchDB, DedupCandidate, MemoryRow } from "./db";
|
|
3
3
|
import type { EmbeddingModel } from "./embeddings";
|
|
4
|
+
import { predictionVerb } from "./prompts";
|
|
4
5
|
|
|
5
|
-
// Near-duplicate thresholds for matcher/prediction dedup at
|
|
6
|
-
// Matches the thatch_find_duplicates threshold (0.85).
|
|
6
|
+
// Near-duplicate thresholds for matcher/prediction/behavior dedup at
|
|
7
|
+
// creation time. Matches the thatch_find_duplicates threshold (0.85).
|
|
7
8
|
const MATCHER_DEDUP_COSINE = 0.85;
|
|
8
9
|
const PREDICTION_DEDUP_COSINE = 0.85;
|
|
10
|
+
const BEHAVIOR_DEDUP_COSINE = 0.85;
|
|
9
11
|
|
|
10
12
|
// Minimum matcher cosine to consider a prediction relevant. Matches the
|
|
11
13
|
// auto-fire threshold in index.ts (PREDICTION_THRESHOLD). The query tool
|
|
12
14
|
// should not return predictions from near-zero-similarity matchers that
|
|
13
15
|
// would never fire in the auto-fire.
|
|
14
|
-
const PREDICTION_QUERY_THRESHOLD = 0.
|
|
16
|
+
const PREDICTION_QUERY_THRESHOLD = 0.60;
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Shared context passed to every tool's execute function. Framework-agnostic -
|
|
@@ -68,7 +70,7 @@ function formatEntry(
|
|
|
68
70
|
return parts.join("\n");
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function formatRecallResult(entry:
|
|
73
|
+
function formatRecallResult(entry: MemoryRow & { _score: number }): string {
|
|
72
74
|
let meta = `store:${entry.store}`;
|
|
73
75
|
if (entry.branch) meta += ` branch:${entry.branch}`;
|
|
74
76
|
if (entry.confidence) meta += ` confidence:${entry.confidence}`;
|
|
@@ -363,25 +365,6 @@ const markCheckedDef: ToolDef = {
|
|
|
363
365
|
},
|
|
364
366
|
};
|
|
365
367
|
|
|
366
|
-
/**
|
|
367
|
-
* Extraction-buffer acknowledgment, with AMQP-style accept/complete roles.
|
|
368
|
-
*
|
|
369
|
-
* Called in a PARENT session after dispatching the fact-extractor, it accepts
|
|
370
|
-
* the buffer: entries move to a holding area and the nudge quiets, but they
|
|
371
|
-
* are not dropped until the extractor completes. Called in a CHILD extractor
|
|
372
|
-
* at the end of its run, it completes the parent's accepted entries -
|
|
373
|
-
* including no-save runs that write no memory. If the child errors or is
|
|
374
|
-
* deleted before either signal, the host requeues the entries so the facts
|
|
375
|
-
* are not lost.
|
|
376
|
-
*
|
|
377
|
-
* The actual state changes happen in the host's post-tool hook
|
|
378
|
-
* (tool.execute.after for opencode, PostToolBatch/appendBatch for MCP) -
|
|
379
|
-
* this tool's execute function is a no-op confirmation. The tool exists so
|
|
380
|
-
* the model has a recognizable tool name to key on. In the MCP path the
|
|
381
|
-
* file-backed queue is consumed on this call (or on any memory_remember),
|
|
382
|
-
* which is durable across interruption because the queue persists on disk
|
|
383
|
-
* until then.
|
|
384
|
-
*/
|
|
385
368
|
/**
|
|
386
369
|
* Fetches the queued extraction payload for a session. The sub-agent calls
|
|
387
370
|
* this with the parent's session ID to retrieve the tool interactions that
|
|
@@ -493,7 +476,7 @@ const predictionQueryDef: ToolDef = {
|
|
|
493
476
|
const scored = ctx.db.scorePredictions(matchers);
|
|
494
477
|
if (scored.length === 0) return "No matching predictions found.";
|
|
495
478
|
return scored.map((s) => {
|
|
496
|
-
const verb = s.evidence_count
|
|
479
|
+
const verb = predictionVerb(s.evidence_count);
|
|
497
480
|
return `[${s.confidence.toFixed(2)} conf, ${s.evidence_count} tests] ` +
|
|
498
481
|
`When ${s.matcher_description}: ${verb} ${s.statement}`;
|
|
499
482
|
}).join("\n");
|
|
@@ -650,6 +633,166 @@ const predictionDeleteDef: ToolDef = {
|
|
|
650
633
|
},
|
|
651
634
|
};
|
|
652
635
|
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
// Behavior engine: LLM self-discipline rules with ham/spam feedback
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
const behaviorCodifyDef: ToolDef = {
|
|
641
|
+
name: "behavior_codify",
|
|
642
|
+
description:
|
|
643
|
+
"Codify a self-discipline rule: when situation X arises, you should " +
|
|
644
|
+
"do Y. Use when you recognize a situation you should react to in a " +
|
|
645
|
+
"specific, repeatable way that is NOT a user preference (use " +
|
|
646
|
+
"prediction_update for those). The rule is about your own operational " +
|
|
647
|
+
"discipline, not what the user wants. Examples: check the whole " +
|
|
648
|
+
"codebase for a library before importing it; investigate disabled " +
|
|
649
|
+
"tests before touching the area; read a large function fully before " +
|
|
650
|
+
"editing it.",
|
|
651
|
+
args: {
|
|
652
|
+
situation: z.string().describe(
|
|
653
|
+
"Description of the situation that triggers this behavior. What context " +
|
|
654
|
+
"or task type makes this rule apply?",
|
|
655
|
+
),
|
|
656
|
+
behavior: z.string().describe(
|
|
657
|
+
"The behavioral rule. What should you do when this situation arises?",
|
|
658
|
+
),
|
|
659
|
+
rationale: z.string().describe(
|
|
660
|
+
"Why you are codifying this rule. What happened that made you realize " +
|
|
661
|
+
"this behavior is worth persisting?",
|
|
662
|
+
),
|
|
663
|
+
store: z.string().optional().describe(
|
|
664
|
+
"Store to write to. Defaults to the project store.",
|
|
665
|
+
),
|
|
666
|
+
},
|
|
667
|
+
async execute(args, ctx) {
|
|
668
|
+
const store = (args.store as string) || ctx.defaultStore;
|
|
669
|
+
const situationText = args.situation as string;
|
|
670
|
+
const behaviorText = args.behavior as string;
|
|
671
|
+
const rationale = args.rationale as string;
|
|
672
|
+
|
|
673
|
+
const matcherEmbed = await ctx.model.passageEmbed(situationText);
|
|
674
|
+
const behaviorEmbed = await ctx.model.passageEmbed(behaviorText);
|
|
675
|
+
|
|
676
|
+
return ctx.db.transaction(() => {
|
|
677
|
+
let matcherId = ctx.db.findNearestBehaviorMatcher(store, matcherEmbed, BEHAVIOR_DEDUP_COSINE)?.id;
|
|
678
|
+
if (!matcherId) matcherId = ctx.db.createBehaviorMatcher(store, situationText, matcherEmbed, ctx.model.name);
|
|
679
|
+
|
|
680
|
+
let behaviorId = ctx.db.findNearestBehavior(store, behaviorEmbed, BEHAVIOR_DEDUP_COSINE)?.id;
|
|
681
|
+
if (!behaviorId) {
|
|
682
|
+
behaviorId = ctx.db.createBehavior(store, behaviorText, rationale, behaviorEmbed, ctx.model.name);
|
|
683
|
+
ctx.db.createBehaviorEdge(matcherId, behaviorId, 1.0);
|
|
684
|
+
ctx.db.addBehaviorProvenance(behaviorId, "codify", rationale);
|
|
685
|
+
return `[codified] ${store} :: "${behaviorText}" for "${situationText}"`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
ctx.db.createBehaviorEdge(matcherId, behaviorId, 1.0);
|
|
689
|
+
ctx.db.addBehaviorProvenance(behaviorId, "codify", rationale);
|
|
690
|
+
const existing = ctx.db.getBehavior(behaviorId);
|
|
691
|
+
return `[linked] "${existing?.statement ?? behaviorText}" for "${situationText}" confidence=${(existing?.confidence ?? 0).toFixed(2)} (${existing?.confirm_count ?? 0}/${existing?.disconfirm_count ?? 0})`;
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const behaviorFeedbackDef: ToolDef = {
|
|
697
|
+
name: "behavior_feedback",
|
|
698
|
+
description:
|
|
699
|
+
"Record ham/spam feedback on a surfaced behavior. When the behavior " +
|
|
700
|
+
"nudge surfaces rules and you evaluate each against the current " +
|
|
701
|
+
"situation, call this with relevant: true (ham) if the rule applies, " +
|
|
702
|
+
"or relevant: false (spam) if it does not. This trains the classifier " +
|
|
703
|
+
"so future nudges are more accurate. Also use when the user corrects " +
|
|
704
|
+
"your behavior and you realize a codified rule led you astray or " +
|
|
705
|
+
"should have been followed.",
|
|
706
|
+
args: {
|
|
707
|
+
behavior: z.string().describe(
|
|
708
|
+
"The behavior statement to provide feedback on. Use behavior_list " +
|
|
709
|
+
"to find the exact text; matching is semantic (cosine >= 0.85).",
|
|
710
|
+
),
|
|
711
|
+
relevant: z.boolean().describe(
|
|
712
|
+
"true (ham) if the behavior is relevant to the current situation. " +
|
|
713
|
+
"false (spam) if it is not relevant.",
|
|
714
|
+
),
|
|
715
|
+
context: z.string().describe(
|
|
716
|
+
"Brief description of the current situation, so the feedback is " +
|
|
717
|
+
"auditable in provenance.",
|
|
718
|
+
),
|
|
719
|
+
store: z.string().optional().describe(
|
|
720
|
+
"Store to search. Defaults to the project store.",
|
|
721
|
+
),
|
|
722
|
+
},
|
|
723
|
+
async execute(args, ctx) {
|
|
724
|
+
const store = (args.store as string) || ctx.defaultStore;
|
|
725
|
+
const behaviorText = args.behavior as string;
|
|
726
|
+
const relevant = args.relevant as boolean;
|
|
727
|
+
const contextText = args.context as string;
|
|
728
|
+
|
|
729
|
+
const behaviorEmbed = await ctx.model.passageEmbed(behaviorText);
|
|
730
|
+
const behavior = ctx.db.findNearestBehavior(store, behaviorEmbed, BEHAVIOR_DEDUP_COSINE);
|
|
731
|
+
if (!behavior) return `No behavior matching "${behaviorText}" found in "${store}".`;
|
|
732
|
+
|
|
733
|
+
const signal = relevant ? "confirm" : "disconfirm";
|
|
734
|
+
ctx.db.transaction(() => {
|
|
735
|
+
ctx.db.adjustBehaviorConfidence(behavior.id, signal);
|
|
736
|
+
ctx.db.addBehaviorProvenance(behavior.id, signal, `${relevant ? "ham" : "spam"}: ${contextText}`);
|
|
737
|
+
});
|
|
738
|
+
const updated = ctx.db.getBehavior(behavior.id);
|
|
739
|
+
return `[${signal}] "${updated?.statement ?? behaviorText}" confidence=${(updated?.confidence ?? 0).toFixed(2)} (${updated?.confirm_count ?? 0}/${updated?.disconfirm_count ?? 0})`;
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const behaviorListDef: ToolDef = {
|
|
744
|
+
name: "behavior_list",
|
|
745
|
+
description:
|
|
746
|
+
"List all codified behaviors with their matchers, confidence, and " +
|
|
747
|
+
"evidence count. For inspection and debugging.",
|
|
748
|
+
args: {
|
|
749
|
+
store: z.string().optional().describe(
|
|
750
|
+
"Which store to list. Defaults to the project store.",
|
|
751
|
+
),
|
|
752
|
+
},
|
|
753
|
+
async execute(args, ctx) {
|
|
754
|
+
const store = (args.store as string) || ctx.defaultStore;
|
|
755
|
+
const behaviors = ctx.db.listBehaviors(store);
|
|
756
|
+
if (behaviors.length === 0) return `No behaviors in "${store}".`;
|
|
757
|
+
return behaviors.map((b) => {
|
|
758
|
+
const matchers = b.matchers.map((m) => ` - "${m.description}" (w:${m.weight})`).join("\n");
|
|
759
|
+
const provenance = ctx.db.getBehaviorProvenance(b.id);
|
|
760
|
+
const provLines = provenance.map((pr) => ` - [${pr.created_at.slice(0, 10)}] ${pr.signal}: ${pr.detail ?? ""}`).join("\n");
|
|
761
|
+
return `[${b.confidence.toFixed(2)} conf, ${b.evidence_count} tests] ${b.statement}` +
|
|
762
|
+
(b.rationale ? `\n rationale: ${b.rationale}` : "") +
|
|
763
|
+
(matchers ? `\n matchers:\n${matchers}` : "") +
|
|
764
|
+
(provLines ? `\n provenance:\n${provLines}` : "");
|
|
765
|
+
}).join("\n\n");
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const behaviorDeleteDef: ToolDef = {
|
|
770
|
+
name: "behavior_delete",
|
|
771
|
+
description:
|
|
772
|
+
"Delete a codified behavior. Useful when a behavior was created in " +
|
|
773
|
+
"error or is no longer relevant. Edges and provenance are deleted " +
|
|
774
|
+
"automatically (cascade).",
|
|
775
|
+
args: {
|
|
776
|
+
statement: z.string().describe(
|
|
777
|
+
"The behavior statement to delete. Use behavior_list to find the " +
|
|
778
|
+
"exact text; matching is semantic (cosine >= 0.85).",
|
|
779
|
+
),
|
|
780
|
+
store: z.string().optional().describe(
|
|
781
|
+
"Store to delete from. Defaults to the project store.",
|
|
782
|
+
),
|
|
783
|
+
},
|
|
784
|
+
async execute(args, ctx) {
|
|
785
|
+
const store = (args.store as string) || ctx.defaultStore;
|
|
786
|
+
const statementText = args.statement as string;
|
|
787
|
+
const behaviorEmbed = await ctx.model.passageEmbed(statementText);
|
|
788
|
+
const behavior = ctx.db.findNearestBehavior(store, behaviorEmbed, BEHAVIOR_DEDUP_COSINE);
|
|
789
|
+
if (!behavior) return `No behavior matching "${statementText}" found in "${store}".`;
|
|
790
|
+
const deleted = ctx.db.deleteBehavior(behavior.id);
|
|
791
|
+
if (!deleted) return `Failed to delete behavior "${behavior.statement}".`;
|
|
792
|
+
return `[deleted] "${behavior.statement}" from "${store}"`;
|
|
793
|
+
},
|
|
794
|
+
};
|
|
795
|
+
|
|
653
796
|
/**
|
|
654
797
|
* All tool definitions, in the order they should be presented to the agent.
|
|
655
798
|
* The opencode plugin wraps each in `tool()`; the MCP server exposes them
|
|
@@ -670,4 +813,8 @@ export const TOOL_DEFS: ToolDef[] = [
|
|
|
670
813
|
predictionUpdateDef,
|
|
671
814
|
predictionListDef,
|
|
672
815
|
predictionDeleteDef,
|
|
816
|
+
behaviorCodifyDef,
|
|
817
|
+
behaviorFeedbackDef,
|
|
818
|
+
behaviorListDef,
|
|
819
|
+
behaviorDeleteDef,
|
|
673
820
|
];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconstructs a Float32Array from a stored BLOB. The Uint8Array bun:sqlite
|
|
3
|
+
* hands back may itself be a view, so honor its offset and length rather than
|
|
4
|
+
* reading the whole backing buffer.
|
|
5
|
+
*/
|
|
6
|
+
export function blobToVector(blob: Uint8Array): Float32Array {
|
|
7
|
+
return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cosine similarity between two Float32Array vectors of equal dimension.
|
|
12
|
+
* Returns a value in [-1, 1]. Normalized embeddings will be close to [0, 1].
|
|
13
|
+
* Throws on dimension mismatch - comparing vectors from different embedding
|
|
14
|
+
* spaces is always a caller bug; callers filter by length first.
|
|
15
|
+
*/
|
|
16
|
+
export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
|
|
17
|
+
if (a.length !== b.length) {
|
|
18
|
+
throw new Error(`cosineSimilarity: dimension mismatch (${a.length} vs ${b.length})`);
|
|
19
|
+
}
|
|
20
|
+
let dot = 0;
|
|
21
|
+
let magA = 0;
|
|
22
|
+
let magB = 0;
|
|
23
|
+
for (let i = 0; i < a.length; i++) {
|
|
24
|
+
dot += a[i] * b[i];
|
|
25
|
+
magA += a[i] * a[i];
|
|
26
|
+
magB += b[i] * b[i];
|
|
27
|
+
}
|
|
28
|
+
const denominator = Math.sqrt(magA) * Math.sqrt(magB);
|
|
29
|
+
return denominator === 0 ? 0 : dot / denominator;
|
|
30
|
+
}
|
package/docs/qa/README.md
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# QA — Test Plan
|
|
2
|
-
|
|
3
|
-
Thatch uses `bun:test`. The full suite runs in well under a second with no
|
|
4
|
-
network access and no writes outside temp directories.
|
|
5
|
-
|
|
6
|
-
## Test conventions
|
|
7
|
-
|
|
8
|
-
- **DB tests** create a real SQLite file in a `mkdtempSync` temp directory,
|
|
9
|
-
removed in `afterEach`. (Real files, not `:memory:` — WAL behavior differs.)
|
|
10
|
-
- **Embedding tests** use `MockEmbeddingModel`: hash-seeded deterministic
|
|
11
|
-
vectors, so identical texts embed identically and unrelated texts land
|
|
12
|
-
near-orthogonal. The real `BgeEmbeddingModel` is never instantiated in
|
|
13
|
-
tests — nothing downloads models.
|
|
14
|
-
- **Git unit tests** exercise `parseGitUrl` on strings; **git integration
|
|
15
|
-
tests** create throwaway repos in temp directories and run real git.
|
|
16
|
-
- **Tool tests** build tools via the factory functions with an injected temp
|
|
17
|
-
DB + mock model, then call `execute` directly.
|
|
18
|
-
- **Plugin tests** call the `server` entry with a mock client and redirect
|
|
19
|
-
`THATCH_DB_PATH` and `XDG_CONFIG_HOME` into a temp directory so skill
|
|
20
|
-
installation never touches the real `~/.config`.
|
|
21
|
-
|
|
22
|
-
## Running tests
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
bun test # all tests
|
|
26
|
-
bun test --watch # watch mode
|
|
27
|
-
mise run check # typecheck + bun test + markdownlint (the CI gate)
|
|
28
|
-
mise run typecheck # tsc alone
|
|
29
|
-
mise run lint-md # markdownlint only
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`mise run check` is the canonical quality gate (also run by CI on push/PR to
|
|
33
|
-
`main`). Typecheck uses `tsconfig.check.json` (which includes the test files
|
|
34
|
-
that the build's `tsconfig.json` excludes). Markdownlint enforces structural
|
|
35
|
-
correctness on `README.md` and `docs/` excluding `docs/plans/`; see
|
|
36
|
-
`.markdownlint-cli2.jsonc` for rule config.
|
|
37
|
-
|
|
38
|
-
## Test files and coverage
|
|
39
|
-
|
|
40
|
-
### `tests/db.test.ts`
|
|
41
|
-
|
|
42
|
-
- `cosineSimilarity`: identical/orthogonal/opposite/zero vectors; throws on
|
|
43
|
-
dimension mismatch
|
|
44
|
-
- Schema creation, store creation/idempotence
|
|
45
|
-
- `slugify`: ASCII, unicode preservation, hash fallback for all-symbol labels
|
|
46
|
-
- `remember`: insert, duplicate rejection without `overwrite`, overwrite,
|
|
47
|
-
metadata, embedding round-trip through a view with non-zero `byteOffset`
|
|
48
|
-
- `recall`: ranking, multi-store, unknown store, empty store list, limit,
|
|
49
|
-
branch scoping, skipping dimension-mismatched entries
|
|
50
|
-
- `listEntries`, `showEntry`, `forgetEntry`
|
|
51
|
-
- Dedup: `findDuplicates` thresholding, checked-pair suppression, verdict
|
|
52
|
-
invalidation on overwrite and forget, dimension-mismatch pair skipping
|
|
53
|
-
- `findSimilar`: threshold/ranking, self-exclusion, dimension skipping, no
|
|
54
|
-
telemetry side effects
|
|
55
|
-
- Recall telemetry: returned rows (and only returned rows) stamped
|
|
56
|
-
- Hygiene queries: stale counting with recall-refresh, branch listing/counts
|
|
57
|
-
- Column migration: pre-telemetry databases gain the new columns on open
|
|
58
|
-
|
|
59
|
-
### `tests/git.test.ts`
|
|
60
|
-
|
|
61
|
-
- `parseGitUrl` across SSH shorthand, HTTPS/HTTP, `ssh://`, `git://`,
|
|
62
|
-
GitLab/self-hosted, hyphens/dots, trailing whitespace, unparseable input
|
|
63
|
-
|
|
64
|
-
### `tests/git-integration.test.ts`
|
|
65
|
-
|
|
66
|
-
- `detectRepo` against real temp git repos: HTTPS remote, SSH remote,
|
|
67
|
-
no-`.git`-suffix remote, no-remote fallback, non-git-directory fallback
|
|
68
|
-
- `listBranches`: local branches listed; empty outside a git repo
|
|
69
|
-
|
|
70
|
-
### `tests/embeddings.test.ts`
|
|
71
|
-
|
|
72
|
-
- `MockEmbeddingModel` contract: dims, loaded, determinism, query/passage
|
|
73
|
-
equivalence, distinct texts → distinct near-orthogonal vectors, model name
|
|
74
|
-
- `BgeEmbeddingModel` (with injected pipeline factory): lazy-load, loaded
|
|
75
|
-
state, concurrent load memoization, error retry, query prefix for
|
|
76
|
-
asymmetric search, passage has no prefix, model name tag, default model name
|
|
77
|
-
|
|
78
|
-
### `tests/tools.test.ts`
|
|
79
|
-
|
|
80
|
-
- `thatch_memory_remember`: happy path, duplicate rejection, overwrite,
|
|
81
|
-
explicit store, metadata
|
|
82
|
-
- `thatch_memory_recall`: default repo+global scope, empty results, store
|
|
83
|
-
override, limit
|
|
84
|
-
- `thatch_memory_list` / `show` / `forget`: found, not-found, store override
|
|
85
|
-
- `thatch_store_list`
|
|
86
|
-
- `thatch_find_duplicates`: surfaces similar pairs, cluster grouping,
|
|
87
|
-
no-match message, store override, skips checked pairs
|
|
88
|
-
- `thatch_dedup_mark_checked`: records verdict, store override
|
|
89
|
-
- Write-time collision warning: fires on similar content, silent for
|
|
90
|
-
unrelated content, never warns against itself on overwrite
|
|
91
|
-
|
|
92
|
-
### `tests/plugin.test.ts`
|
|
93
|
-
|
|
94
|
-
- `server` export shape: all thirteen tools, every hook present, tools carry
|
|
95
|
-
description/args/execute
|
|
96
|
-
- System transform and compaction hooks append their text
|
|
97
|
-
- `tool.execute.after` → `chat.message` extraction round-trip: nudge carries
|
|
98
|
-
the JSON payload, buffers are per-session, flush is one-shot, `thatch_*`
|
|
99
|
-
tools are excluded from buffering
|
|
100
|
-
- Skill files land under the redirected `XDG_CONFIG_HOME`
|
|
101
|
-
- `event` handler: calls `client.session.prompt` on `session.created` with
|
|
102
|
-
correct session ID and nudge text; ignores non-`session.created` events
|
|
103
|
-
- `sessionStartReminder`: includes store name and recall instructions
|
|
104
|
-
- Recall nudge via `chat.message`: surfaces when prompt matches a stored
|
|
105
|
-
memory, no nudge for unrelated or short prompts, extraction priority
|
|
106
|
-
- Prediction auto-fire via `chat.message`: surfaces when prompt matches a
|
|
107
|
-
stored matcher, no nudge for unrelated prompts, prediction and recall
|
|
108
|
-
nudges fire independently as separate synthetic parts
|
|
109
|
-
|
|
110
|
-
### `tests/tool-defs.test.ts`
|
|
111
|
-
|
|
112
|
-
- Tool count (13) and name list
|
|
113
|
-
- Zod schema validation for each tool's args
|
|
114
|
-
- Execute functions: `memory_remember` save/duplicate/overwrite/archived,
|
|
115
|
-
`memory_recall` default/empty/archived, `memory_list`/`show`/`forget`,
|
|
116
|
-
`store_list`, `find_duplicates` clean store
|
|
117
|
-
- Prediction execute functions: `prediction_update` create/confirm/disconfirm/
|
|
118
|
-
create-on-existing (dedup links without disconfirming), `prediction_query`
|
|
119
|
-
matching/unrelated, `prediction_list` with provenance, `prediction_delete`
|
|
120
|
-
found/not-found
|
|
121
|
-
|
|
122
|
-
### `tests/prediction.test.ts`
|
|
123
|
-
|
|
124
|
-
- Schema: tables exist, `populationP0` fallback on empty store
|
|
125
|
-
- Matcher creation and lookup: `findMatchers` ranking, `findNearestMatcher`
|
|
126
|
-
threshold/dedup
|
|
127
|
-
- Prediction creation: `createPrediction` seeds at p0, `adjustConfidence`
|
|
128
|
-
confirm/disconfirm/soft/multiple, non-existent ID no-op
|
|
129
|
-
- Edges and scoring: `createEdge` links, `scorePredictions` empty/dedup/
|
|
130
|
-
weight-preservation, `scorePredictionNudge` threshold filtering + dedup
|
|
131
|
-
- `findNearestPrediction`: store-wide search, threshold, cross-matcher
|
|
132
|
-
- Provenance: `addProvenance` + `getProvenance` read-back, `deletePrediction`
|
|
133
|
-
cascade to edges and provenance
|
|
134
|
-
- `listPredictions`: sorted by confidence, matchers included
|
|
135
|
-
- `populationP0`: under-20 fallback, population hit rate with sufficient evidence
|
|
136
|
-
|
|
137
|
-
### `tests/hygiene.test.ts`
|
|
138
|
-
|
|
139
|
-
- `hygieneReport`: silent on a healthy store; counts duplicate candidates,
|
|
140
|
-
stale memories, and orphaned branch-scoped memories (against a real temp
|
|
141
|
-
git repo); skips the branch check outside a git repo
|
|
142
|
-
- `sessionStartReminder` hygiene block: appended when present, omitted when
|
|
143
|
-
null, single-arg call unchanged
|
|
144
|
-
|
|
145
|
-
## Known gaps
|
|
146
|
-
|
|
147
|
-
- `BgeEmbeddingModel`'s default `PipelineFactory` (the real HF import) is
|
|
148
|
-
untested — exercising it means downloading the real model, which violates
|
|
149
|
-
the no-network rule. The lazy-load logic is tested with an injected mock
|
|
150
|
-
factory. The real model is exercised indirectly by any real opencode use.
|
|
151
|
-
- The skill install failure path in `index.ts` has no test (would require
|
|
152
|
-
mocking `fs` to force a write error).
|
|
153
|
-
- `bin/thatch` has no automated tests; it is a thin shell over `db.ts`.
|
|
154
|
-
|
|
155
|
-
## QA use-case suite
|
|
156
|
-
|
|
157
|
-
End-to-end QA scenarios live in `tests/qa/` as bun test files, not docs.
|
|
158
|
-
Each use case is a `uc-NNN-*.test.ts` file that defines preconditions,
|
|
159
|
-
steps, and expected results as string constants, then registers itself
|
|
160
|
-
via `registerUseCase` from `tests/qa/runner.ts`.
|
|
161
|
-
|
|
162
|
-
Most use cases shell out to `opencode run` with an isolated repo copy
|
|
163
|
-
(temp DB, temp config, temp home). Automatable use cases override the
|
|
164
|
-
`run` function with direct CLI/bun assertions. Manual-only use cases
|
|
165
|
-
(compaction, visual TUI) are skipped automatically.
|
|
166
|
-
|
|
167
|
-
Run the suite:
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
mise run qa # full run (spawns opencode sessions, costs model tokens)
|
|
171
|
-
mise run qa-dry-run # list use cases without spawning sessions
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
The regular test suite (`mise run check`) excludes `tests/qa/` by using
|
|
175
|
-
a flat glob (`bun test tests/*.test.ts`).
|