@polderlabs/bizar 6.3.0 → 7.0.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/.claude/hooks/sessionend-recall.mjs +3 -3
- package/.claude/hooks/worker-suggest.mjs +110 -0
- package/.claude/settings.json +9 -0
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
- package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
- package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
- package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
- package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
- package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
- package/bizar-dash/dist/index.html +12 -10
- package/bizar-dash/dist/mobile.html +7 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +11 -0
- package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
- package/bizar-dash/src/server/memory-distillation.mjs +296 -0
- package/bizar-dash/src/server/routes/distill.mjs +129 -0
- package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
- package/bizar-dash/src/web/App.tsx +4 -0
- package/bizar-dash/src/web/components/Tag.tsx +18 -5
- package/bizar-dash/src/web/components/Topbar.tsx +3 -0
- package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
- package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
- package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
- package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
- package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
- package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
- package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
- package/bizar-dash/src/web/lib/types.ts +25 -0
- package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
- package/bizar-dash/src/web/styles/main.css +251 -0
- package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
- package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
- package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
- package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
- package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
- package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
- package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
- package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
- package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
- package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
- package/bizar-dash/src/web/ui/controls/controls.css +454 -0
- package/bizar-dash/src/web/ui/controls/index.ts +33 -0
- package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
- package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
- package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
- package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
- package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
- package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
- package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
- package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
- package/bizar-dash/src/web/ui/data/data.css +535 -0
- package/bizar-dash/src/web/ui/data/index.ts +31 -0
- package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
- package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
- package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
- package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
- package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
- package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
- package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
- package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
- package/bizar-dash/src/web/ui/index.ts +56 -0
- package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
- package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
- package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
- package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
- package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
- package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
- package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
- package/bizar-dash/src/web/ui/layout/index.ts +33 -0
- package/bizar-dash/src/web/ui/layout/layout.css +413 -0
- package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
- package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
- package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
- package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
- package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
- package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
- package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
- package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
- package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
- package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
- package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
- package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
- package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
- package/bizar-dash/src/web/ui/styles/globals.css +86 -0
- package/bizar-dash/src/web/ui/styles/reset.css +125 -0
- package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
- package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
- package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
- package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
- package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
- package/bizar-dash/src/web/views/Agents.tsx +506 -266
- package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
- package/bizar-dash/src/web/views/Overview.tsx +996 -448
- package/bizar-dash/src/web/views/Tasks.tsx +277 -273
- package/bizar-dash/tests/communication-log.test.tsx +67 -0
- package/bizar-dash/tests/dep-graph.test.tsx +40 -0
- package/bizar-dash/tests/event-log.test.tsx +61 -0
- package/bizar-dash/tests/goal-input.test.tsx +72 -0
- package/bizar-dash/tests/goap-planner.test.ts +68 -0
- package/bizar-dash/tests/quality-gates.test.tsx +59 -0
- package/bizar-dash/tests/setup.ts +31 -1
- package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
- package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
- package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
- package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
- package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
- package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
- package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
- package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
- package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
- package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
- package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
- package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
- package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
- package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
- package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
- package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
- package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
- package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
- package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
- package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
- package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
- package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
- package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
- package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
- package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
- package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
- package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
- package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
- package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
- package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
- package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
- package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
- package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
- package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
- package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
- package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
- package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
- package/bizar-dash/tests/views/Agents.test.tsx +359 -0
- package/bizar-dash/tests/views/Overview.test.tsx +413 -0
- package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
- package/bizar-dash/vitest.config.ts +3 -1
- package/cli/__tests__/cost-gate.test.mjs +301 -0
- package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
- package/cli/bin.mjs +38 -0
- package/cli/commands/claim.mjs +282 -0
- package/cli/commands/cost.mjs +279 -0
- package/cli/cost-gate.mjs +501 -0
- package/cli/feature-list-bridge.mjs +400 -0
- package/cli/memory-constants.mjs +17 -0
- package/cli/worker-dispatcher.mjs +259 -0
- package/cli/worker-dispatcher.test.mjs +204 -0
- package/config/trigger-patterns.json +186 -0
- package/package.json +2 -2
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/agent-registry.ts +304 -0
- package/packages/sdk/src/consensus/byzantine.ts +587 -0
- package/packages/sdk/src/consensus/index.ts +147 -0
- package/packages/sdk/src/consensus/queen.ts +173 -0
- package/packages/sdk/src/consensus/types.ts +199 -0
- package/packages/sdk/src/federation/audit.ts +156 -0
- package/packages/sdk/src/federation/budget.ts +269 -0
- package/packages/sdk/src/federation/envelope.ts +144 -0
- package/packages/sdk/src/federation/hmac.ts +160 -0
- package/packages/sdk/src/federation/index.ts +443 -0
- package/packages/sdk/src/federation/pii.ts +241 -0
- package/packages/sdk/src/federation/policy.ts +112 -0
- package/packages/sdk/src/federation/trust.ts +146 -0
- package/packages/sdk/src/index.ts +67 -0
- package/packages/sdk/src/mcp/server.ts +351 -1
- package/packages/sdk/src/router/codemod-intent.ts +125 -0
- package/packages/sdk/src/router/index.ts +247 -0
- package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
- package/packages/sdk/src/router/memory-distillation.ts +249 -0
- package/packages/sdk/src/router/model-router.ts +235 -0
- package/packages/sdk/src/router/q-learning-router.ts +304 -0
- package/packages/sdk/src/swarm-topology.ts +279 -0
- package/packages/sdk/tests/agent-registry.test.ts +285 -0
- package/packages/sdk/tests/consensus.test.ts +376 -0
- package/packages/sdk/tests/federation/audit.test.ts +164 -0
- package/packages/sdk/tests/federation/budget.test.ts +189 -0
- package/packages/sdk/tests/federation/envelope.test.ts +120 -0
- package/packages/sdk/tests/federation/hmac.test.ts +194 -0
- package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
- package/packages/sdk/tests/federation/pii.test.ts +106 -0
- package/packages/sdk/tests/federation/policy.test.ts +140 -0
- package/packages/sdk/tests/federation/trust.test.ts +124 -0
- package/packages/sdk/tests/mcp-tools.test.ts +204 -0
- package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
- package/packages/sdk/tests/model-router.test.mjs +108 -0
- package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
- package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
- package/packages/sdk/tests/router.test.mjs +91 -0
- package/packages/sdk/tests/sdk.test.mjs +26 -2
- package/packages/sdk/tests/swarm-topology.test.ts +258 -0
- package/scripts/bh-full-e2e.mjs +11 -15
- package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
- package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
- package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
- package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
- package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
- package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
- package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
- package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
- package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
- package/packages/sdk/package-lock.json +0 -32
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the RETRIEVE → JUDGE → DISTILL → CONSOLIDATE pipeline (F-033 / ADR-174).
|
|
3
|
+
*
|
|
4
|
+
* Roundtrip: write 3 memory entries of kind `coding_convention`,
|
|
5
|
+
* run the distiller, assert ≥1 promoted pattern with
|
|
6
|
+
* `provenance_tier` set.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from "vitest";
|
|
10
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
runDistillation,
|
|
15
|
+
retrieve,
|
|
16
|
+
judge,
|
|
17
|
+
PROVENANCE_TIERS,
|
|
18
|
+
} from "../src/router/memory-distillation.js";
|
|
19
|
+
import { writeNote } from "../dist/memory/index.js";
|
|
20
|
+
|
|
21
|
+
function makeEntry(body, opts = {}) {
|
|
22
|
+
return {
|
|
23
|
+
memory_id: opts.id ?? `note-${Math.random().toString(36).slice(2, 8)}`,
|
|
24
|
+
frontmatter: {
|
|
25
|
+
kind: opts.kind ?? "coding_convention",
|
|
26
|
+
createdAt: opts.createdAt ?? new Date().toISOString(),
|
|
27
|
+
...opts.frontmatter,
|
|
28
|
+
},
|
|
29
|
+
body,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("memory-distillation — RETRIEVE", () => {
|
|
34
|
+
test("short body without code block is 'simple'", () => {
|
|
35
|
+
const r = retrieve(makeEntry("Use const for module exports.", { kind: "coding_convention" }));
|
|
36
|
+
expect(r.complexity).toBe("simple");
|
|
37
|
+
expect(r.hasCodeBlock).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("body with a fenced code block is 'complex'", () => {
|
|
41
|
+
const r = retrieve(makeEntry("Pattern:\n```ts\nconst a = 1;\n```"));
|
|
42
|
+
expect(r.complexity).toBe("complex");
|
|
43
|
+
expect(r.hasCodeBlock).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("frontmatter tag `test-exec` flips testExec=true", () => {
|
|
47
|
+
const r = retrieve(makeEntry("x", { frontmatter: { tags: ["test-exec"] } }));
|
|
48
|
+
expect(r.testExec).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("frontmatter `kind: feedback` flips testExec=true", () => {
|
|
52
|
+
const r = retrieve(makeEntry("x", { frontmatter: { kind: "feedback" } }));
|
|
53
|
+
expect(r.testExec).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("memory-distillation — JUDGE / provenance_tier", () => {
|
|
58
|
+
test("testExec=true → oracle:test-exec", () => {
|
|
59
|
+
const r = retrieve(makeEntry("x", { frontmatter: { tags: ["test-exec"] } }));
|
|
60
|
+
expect(judge(makeEntry("x"), r)).toBe("oracle:test-exec");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("testExec=false → proxy:structural", () => {
|
|
64
|
+
const r = retrieve(makeEntry("x", { kind: "coding_convention" }));
|
|
65
|
+
expect(judge(makeEntry("x"), r)).toBe("proxy:structural");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("memory-distillation — RUN (4-step pipeline)", () => {
|
|
70
|
+
test("3 coding_convention entries → ≥1 promoted pattern with provenance_tier", () => {
|
|
71
|
+
const entries = [
|
|
72
|
+
makeEntry("Always use const for module-level exports.", { kind: "coding_convention" }),
|
|
73
|
+
makeEntry("Prefer async/await over raw promise chains.", { kind: "coding_convention" }),
|
|
74
|
+
makeEntry(
|
|
75
|
+
"Document every exported function with a JSDoc block.",
|
|
76
|
+
{ kind: "coding_convention", frontmatter: { kind: "coding_convention", tags: ["test-exec"] } },
|
|
77
|
+
),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const result = runDistillation(entries);
|
|
81
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
82
|
+
// The third entry carries the `test-exec` tag → oracle tier →
|
|
83
|
+
// eligible for promotion.
|
|
84
|
+
const promoted = result.promoted;
|
|
85
|
+
expect(promoted.length).toBeGreaterThanOrEqual(1);
|
|
86
|
+
expect(promoted[0].provenance_tier).toBe("oracle:test-exec");
|
|
87
|
+
expect(promoted[0].promoted).toBe(true);
|
|
88
|
+
expect(result.byTier["oracle:test-exec"]).toBeGreaterThanOrEqual(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("all-proxy input still produces patterns, but none promoted", () => {
|
|
92
|
+
const entries = [
|
|
93
|
+
makeEntry("Pattern: keep imports sorted.", { kind: "task_summary" }),
|
|
94
|
+
makeEntry("Use: lint before commit.", { kind: "task_summary" }),
|
|
95
|
+
];
|
|
96
|
+
const result = runDistillation(entries);
|
|
97
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
98
|
+
expect(result.promoted.length).toBe(0);
|
|
99
|
+
expect(result.byTier["proxy:structural"]).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("empty input → empty result with zero counts", () => {
|
|
103
|
+
const result = runDistillation([]);
|
|
104
|
+
expect(result.patterns).toEqual([]);
|
|
105
|
+
expect(result.promoted).toEqual([]);
|
|
106
|
+
for (const tier of PROVENANCE_TIERS) {
|
|
107
|
+
expect(result.byTier[tier]).toBe(0);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("non-pattern kinds (e.g. user_preference) are not distilled", () => {
|
|
112
|
+
const entries = [
|
|
113
|
+
makeEntry("I prefer dark mode.", { kind: "user_preference" }),
|
|
114
|
+
makeEntry("I prefer tabs.", { kind: "user_preference" }),
|
|
115
|
+
];
|
|
116
|
+
const result = runDistillation(entries);
|
|
117
|
+
expect(result.patterns.length).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("run is idempotent (same input → same patterns)", () => {
|
|
121
|
+
const entries = [
|
|
122
|
+
makeEntry("Use const for top-level exports.", { kind: "coding_convention", id: "a" }),
|
|
123
|
+
makeEntry(
|
|
124
|
+
"Use const for top-level exports in TypeScript modules.",
|
|
125
|
+
{ kind: "coding_convention", frontmatter: { tags: ["test-exec"] }, id: "b" },
|
|
126
|
+
),
|
|
127
|
+
];
|
|
128
|
+
const r1 = runDistillation(entries);
|
|
129
|
+
const r2 = runDistillation(entries);
|
|
130
|
+
expect(r2.patterns.length).toBe(r1.patterns.length);
|
|
131
|
+
expect(r2.promoted.map((p) => p.id).sort()).toEqual(r1.promoted.map((p) => p.id).sort());
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("memory-distillation — roundtrip via in-process vault", () => {
|
|
136
|
+
test("write 3 coding_convention notes → run distiller → promoted pattern exists", async () => {
|
|
137
|
+
const vault = mkdtempSync(join(tmpdir(), "bizar-f033-"));
|
|
138
|
+
try {
|
|
139
|
+
// 1. Seed: write 3 coding_convention notes — one of them is
|
|
140
|
+
// marked `test-exec` to drive the oracle tier.
|
|
141
|
+
writeNote(vault, "conv-1.md", {
|
|
142
|
+
title: "Always use const for module-level exports",
|
|
143
|
+
kind: "coding_convention",
|
|
144
|
+
createdAt: new Date().toISOString(),
|
|
145
|
+
}, "Always use `const` for module-level exports.");
|
|
146
|
+
writeNote(vault, "conv-2.md", {
|
|
147
|
+
title: "Prefer async/await over raw promise chains",
|
|
148
|
+
kind: "coding_convention",
|
|
149
|
+
createdAt: new Date().toISOString(),
|
|
150
|
+
}, "Prefer `async`/`await` over raw `.then()` chains.");
|
|
151
|
+
writeNote(vault, "conv-3.md", {
|
|
152
|
+
title: "Document exported functions",
|
|
153
|
+
kind: "coding_convention",
|
|
154
|
+
createdAt: new Date().toISOString(),
|
|
155
|
+
tags: ["test-exec"],
|
|
156
|
+
}, "Document every exported function with a JSDoc block.");
|
|
157
|
+
|
|
158
|
+
// 2. Read them back via the SDK in-process vault helpers.
|
|
159
|
+
const vaultModule = await import("../dist/memory/index.js");
|
|
160
|
+
const notes = vaultModule.listNotes(vault, "", 100);
|
|
161
|
+
expect(notes.length).toBe(3);
|
|
162
|
+
|
|
163
|
+
// 3. Translate to the distiller's input shape.
|
|
164
|
+
const entries = notes.map((n) => ({
|
|
165
|
+
memory_id: n.relPath,
|
|
166
|
+
relPath: n.relPath,
|
|
167
|
+
frontmatter: n.frontmatter ?? {},
|
|
168
|
+
body: n.body ?? "",
|
|
169
|
+
kind: n.frontmatter?.kind ?? n.frontmatter?.type,
|
|
170
|
+
createdAt: n.frontmatter?.createdAt,
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
// 4. Run the distiller and assert the promoted pattern.
|
|
174
|
+
const result = runDistillation(entries);
|
|
175
|
+
expect(result.patterns.length).toBeGreaterThanOrEqual(1);
|
|
176
|
+
const promoted = result.promoted;
|
|
177
|
+
expect(promoted.length).toBeGreaterThanOrEqual(1);
|
|
178
|
+
expect(promoted[0].provenance_tier).toBe("oracle:test-exec");
|
|
179
|
+
expect(promoted[0].promoted).toBe(true);
|
|
180
|
+
// Provenance tier is one of the allowed ADR-174 values.
|
|
181
|
+
expect(PROVENANCE_TIERS).toContain(promoted[0].provenance_tier);
|
|
182
|
+
} finally {
|
|
183
|
+
rmSync(vault, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Thompson-sampling bandit model router (F-033).
|
|
3
|
+
*
|
|
4
|
+
* - Codemod short-circuit returns `{tier: 'flash', codemodIntent}`.
|
|
5
|
+
* - Default priors make `flash` the most-likely pick.
|
|
6
|
+
* - recordOutcome() shifts the Beta(α, β) priors.
|
|
7
|
+
* - recordOutcome with seed gives deterministic picks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { ModelRouter } from "../src/router/model-router.js";
|
|
12
|
+
|
|
13
|
+
describe("ModelRouter — codemod short-circuit", () => {
|
|
14
|
+
test("var-to-const short-circuits to flash with codemodIntent", () => {
|
|
15
|
+
const r = new ModelRouter({ seed: 42 });
|
|
16
|
+
const d = r.route("convert var to const");
|
|
17
|
+
expect(d.tier).toBe("flash");
|
|
18
|
+
expect(d.codemodIntent).toBe("var-to-const");
|
|
19
|
+
expect(d.confidence).toBe(1.0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("remove-console short-circuits to flash with codemodIntent", () => {
|
|
23
|
+
const r = new ModelRouter({ seed: 42 });
|
|
24
|
+
const d = r.route("strip console.log statements");
|
|
25
|
+
expect(d.tier).toBe("flash");
|
|
26
|
+
expect(d.codemodIntent).toBe("remove-console");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("add-logging short-circuits to flash with codemodIntent", () => {
|
|
30
|
+
const r = new ModelRouter({ seed: 42 });
|
|
31
|
+
const d = r.route("add logging to the handler");
|
|
32
|
+
expect(d.tier).toBe("flash");
|
|
33
|
+
expect(d.codemodIntent).toBe("add-logging");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("ModelRouter — Thompson sampling", () => {
|
|
38
|
+
test("non-codemod prompt returns a valid tier", () => {
|
|
39
|
+
const r = new ModelRouter({ seed: 42 });
|
|
40
|
+
const d = r.route("design a database schema for inventory");
|
|
41
|
+
expect(["flash", "mid", "expensive"]).toContain(d.tier);
|
|
42
|
+
expect(d.confidence).toBeGreaterThanOrEqual(0);
|
|
43
|
+
expect(d.confidence).toBeLessThanOrEqual(1);
|
|
44
|
+
expect(d.codemodIntent).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("default priors favor flash (alpha=2, beta=1)", () => {
|
|
48
|
+
const r = new ModelRouter({ seed: 1 });
|
|
49
|
+
const priors = r.getPriors();
|
|
50
|
+
// The default-prior math (matches ADR-026 §BANDIT_REWARDS note).
|
|
51
|
+
expect(priors.flash.alpha).toBe(2);
|
|
52
|
+
expect(priors.flash.beta).toBe(1);
|
|
53
|
+
expect(priors.mid.alpha).toBe(1);
|
|
54
|
+
expect(priors.mid.beta).toBe(1);
|
|
55
|
+
expect(priors.expensive.alpha).toBe(1);
|
|
56
|
+
expect(priors.expensive.beta).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("many flash successes shift priors toward flash", () => {
|
|
60
|
+
const r = new ModelRouter({ seed: 42 });
|
|
61
|
+
for (let i = 0; i < 30; i++) r.recordOutcome("flash", true);
|
|
62
|
+
const priors = r.getPriors();
|
|
63
|
+
expect(priors.flash.alpha).toBeGreaterThan(2);
|
|
64
|
+
expect(priors.flash.beta).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("flash failures increment β", () => {
|
|
68
|
+
const r = new ModelRouter({ seed: 42 });
|
|
69
|
+
for (let i = 0; i < 5; i++) r.recordOutcome("flash", false);
|
|
70
|
+
const priors = r.getPriors();
|
|
71
|
+
expect(priors.flash.alpha).toBe(2);
|
|
72
|
+
expect(priors.flash.beta).toBe(1 + 5);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("seeded RNG produces deterministic pick", () => {
|
|
76
|
+
const a = new ModelRouter({ seed: 7 });
|
|
77
|
+
const b = new ModelRouter({ seed: 7 });
|
|
78
|
+
const prompt = "explain what this function does";
|
|
79
|
+
// Run several samples; at least one must agree (probabilistically
|
|
80
|
+
// very near 1.0).
|
|
81
|
+
let matches = 0;
|
|
82
|
+
for (let i = 0; i < 20; i++) {
|
|
83
|
+
const da = a.route(prompt);
|
|
84
|
+
const db = b.route(prompt);
|
|
85
|
+
if (da.tier === db.tier) matches += 1;
|
|
86
|
+
}
|
|
87
|
+
expect(matches).toBeGreaterThanOrEqual(18);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("expensive success is devalued (alpha += 0.4)", () => {
|
|
91
|
+
const r = new ModelRouter({ seed: 42 });
|
|
92
|
+
r.recordOutcome("expensive", true);
|
|
93
|
+
const priors = r.getPriors();
|
|
94
|
+
expect(priors.expensive.alpha).toBeCloseTo(1.4);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("ModelRouter — overrides", () => {
|
|
99
|
+
test("priors override applies per-tier", () => {
|
|
100
|
+
const r = new ModelRouter({
|
|
101
|
+
priors: { expensive: { alpha: 100, beta: 1 } },
|
|
102
|
+
seed: 1,
|
|
103
|
+
});
|
|
104
|
+
const priors = r.getPriors();
|
|
105
|
+
expect(priors.expensive.alpha).toBe(100);
|
|
106
|
+
expect(priors.flash.alpha).toBe(2); // default still in place
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Q-learning agent router (F-033).
|
|
3
|
+
*
|
|
4
|
+
* - selectAgent() returns one of the 8 hard-coded Bizar agent names.
|
|
5
|
+
* - Codemod prompts route to heimdall (the routine-implementation agent).
|
|
6
|
+
* - recordOutcome() updates the Q-table (snapshot mean shifts).
|
|
7
|
+
* - LRU caching: repeated tasks return the same pick without burning
|
|
8
|
+
* exploration budget.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect } from "vitest";
|
|
12
|
+
import { QLearningRouter, AGENT_ACTIONS } from "../src/router/q-learning-router.js";
|
|
13
|
+
|
|
14
|
+
describe("QLearningRouter — action set", () => {
|
|
15
|
+
test("AGENT_ACTIONS lists 8 Bizar agents", () => {
|
|
16
|
+
expect(AGENT_ACTIONS.length).toBe(8);
|
|
17
|
+
expect(AGENT_ACTIONS).toContain("odin");
|
|
18
|
+
expect(AGENT_ACTIONS).toContain("heimdall");
|
|
19
|
+
expect(AGENT_ACTIONS).toContain("forseti");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("QLearningRouter — codemod short-circuit", () => {
|
|
24
|
+
test("var-to-const picks heimdall", () => {
|
|
25
|
+
const r = new QLearningRouter({ seed: 42, epsilon: 0 });
|
|
26
|
+
expect(r.selectAgent("convert var to const").agent).toBe("heimdall");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("remove-console picks heimdall", () => {
|
|
30
|
+
const r = new QLearningRouter({ seed: 42, epsilon: 0 });
|
|
31
|
+
expect(r.selectAgent("remove console.log calls").agent).toBe("heimdall");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("add-logging picks heimdall", () => {
|
|
35
|
+
const r = new QLearningRouter({ seed: 42, epsilon: 0 });
|
|
36
|
+
expect(r.selectAgent("add logging to the handler").agent).toBe("heimdall");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("QLearningRouter — bandit behavior", () => {
|
|
41
|
+
test("non-codemod task returns a valid agent", () => {
|
|
42
|
+
const r = new QLearningRouter({ seed: 42, epsilon: 0.5 });
|
|
43
|
+
const d = r.selectAgent("explain the architecture of this codebase");
|
|
44
|
+
expect(AGENT_ACTIONS).toContain(d.agent);
|
|
45
|
+
expect(d.confidence).toBeGreaterThanOrEqual(0);
|
|
46
|
+
expect(d.confidence).toBeLessThanOrEqual(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("epsilon=0 deterministic given seeded RNG", () => {
|
|
50
|
+
const a = new QLearningRouter({ seed: 99, epsilon: 0 });
|
|
51
|
+
const b = new QLearningRouter({ seed: 99, epsilon: 0 });
|
|
52
|
+
const task = "implement a new cache layer for the storage engine";
|
|
53
|
+
// Different RNG state path; the LRU cache for the first call
|
|
54
|
+
// should fire identically. Subsequent calls fall back to bandit.
|
|
55
|
+
expect(a.selectAgent(task).agent).toBe(b.selectAgent(task).agent);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("recordOutcome shifts the Q snapshot toward the rewarded agent", () => {
|
|
59
|
+
const r = new QLearningRouter({ seed: 7, epsilon: 0 });
|
|
60
|
+
const before = r.snapshot().find((s) => s.agent === "thor")?.qMean ?? 0.5;
|
|
61
|
+
for (let i = 0; i < 50; i++) r.recordOutcome("thor", true);
|
|
62
|
+
const after = r.snapshot().find((s) => s.agent === "thor")?.qMean ?? 0.5;
|
|
63
|
+
expect(after).toBeGreaterThan(before);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("failures shift the Q mean downward", () => {
|
|
67
|
+
const r = new QLearningRouter({ seed: 7, epsilon: 0 });
|
|
68
|
+
for (let i = 0; i < 50; i++) r.recordOutcome("frigg", false);
|
|
69
|
+
const after = r.snapshot().find((s) => s.agent === "frigg")?.qMean ?? 0.5;
|
|
70
|
+
expect(after).toBeLessThan(0.5);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("recordOutcome is a no-op for unknown agents", () => {
|
|
74
|
+
const r = new QLearningRouter({ seed: 7, epsilon: 0 });
|
|
75
|
+
const before = r.snapshot();
|
|
76
|
+
r.recordOutcome("not-a-real-agent", true);
|
|
77
|
+
const after = r.snapshot();
|
|
78
|
+
expect(after).toEqual(before);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("QLearningRouter — LRU cache", () => {
|
|
83
|
+
test("repeated task returns the cached decision", () => {
|
|
84
|
+
const r = new QLearningRouter({ seed: 11, epsilon: 0 });
|
|
85
|
+
const task = "review this PR for security concerns";
|
|
86
|
+
const first = r.selectAgent(task);
|
|
87
|
+
const second = r.selectAgent(task);
|
|
88
|
+
// The LRU returns the exact stored object — same agent, same confidence.
|
|
89
|
+
expect(second.agent).toBe(first.agent);
|
|
90
|
+
expect(second.confidence).toBe(first.confidence);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("QLearningRouter — empty input safety", () => {
|
|
95
|
+
test("null task returns the safe odin fallback", () => {
|
|
96
|
+
const r = new QLearningRouter({ seed: 7 });
|
|
97
|
+
const d = r.selectAgent("");
|
|
98
|
+
expect(d.agent).toBe("odin");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the router orchestrator (F-033).
|
|
3
|
+
*
|
|
4
|
+
* Verifies the precedence chain:
|
|
5
|
+
* explicit > codemod > q-learning > bandit > default
|
|
6
|
+
*
|
|
7
|
+
* Plus the surfaced-tags contract:
|
|
8
|
+
* [CODEMOD_AVAILABLE] <intent>
|
|
9
|
+
* [TASK_MODEL_RECOMMENDATION] <tier>
|
|
10
|
+
*
|
|
11
|
+
* Plus persistence: saveTo() writes state files; loadFrom() rehydrates
|
|
12
|
+
* the same priors.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, test, expect } from "vitest";
|
|
16
|
+
import { mkdtempSync, existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
decideAgentWith,
|
|
22
|
+
getRouter,
|
|
23
|
+
ModelRouter,
|
|
24
|
+
QLearningRouter,
|
|
25
|
+
codemodTag,
|
|
26
|
+
tierTag,
|
|
27
|
+
DEFAULT_MODEL_STATE_PATH,
|
|
28
|
+
DEFAULT_Q_STATE_PATH,
|
|
29
|
+
} from "../src/router/index.js";
|
|
30
|
+
|
|
31
|
+
describe("decideAgentWith — precedence chain", () => {
|
|
32
|
+
test("explicit agent wins (when known)", () => {
|
|
33
|
+
const mr = new ModelRouter({ seed: 42 });
|
|
34
|
+
const qr = new QLearningRouter({ seed: 42 });
|
|
35
|
+
const d = decideAgentWith(mr, qr, {
|
|
36
|
+
task: "convert var to const",
|
|
37
|
+
explicitAgent: "odin",
|
|
38
|
+
});
|
|
39
|
+
expect(d.agent).toBe("odin");
|
|
40
|
+
expect(d.agentConfidence).toBe(1.0);
|
|
41
|
+
expect(d.codemodIntent).toBeNull();
|
|
42
|
+
// Explicit path still emits a [TASK_MODEL_RECOMMENDATION] tag.
|
|
43
|
+
expect(d.surfacedTags.some((t) => t.startsWith("[TASK_MODEL_RECOMMENDATION]"))).toBe(true);
|
|
44
|
+
expect(d.surfacedTags.some((t) => t.startsWith("[CODEMOD_AVAILABLE]"))).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("codemod intent short-circuits before q-learning", () => {
|
|
48
|
+
const mr = new ModelRouter({ seed: 42 });
|
|
49
|
+
const qr = new QLearningRouter({ seed: 42 });
|
|
50
|
+
const d = decideAgentWith(mr, qr, { task: "convert var to const" });
|
|
51
|
+
expect(d.agent).toBe("heimdall");
|
|
52
|
+
expect(d.agentConfidence).toBe(1.0);
|
|
53
|
+
expect(d.codemodIntent).toBe("var-to-const");
|
|
54
|
+
expect(d.modelTier).toBe("flash");
|
|
55
|
+
// Both tags should be surfaced for a codemod hit.
|
|
56
|
+
expect(d.surfacedTags.some((t) => t.includes("CODEMOD_AVAILABLE"))).toBe(true);
|
|
57
|
+
expect(d.surfacedTags.some((t) => t.includes("TASK_MODEL_RECOMMENDATION"))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("non-codemod task routes via q-learning + bandit", () => {
|
|
61
|
+
const mr = new ModelRouter({ seed: 42 });
|
|
62
|
+
const qr = new QLearningRouter({ seed: 42 });
|
|
63
|
+
const d = decideAgentWith(mr, qr, { task: "design a database schema" });
|
|
64
|
+
expect(["odin", "frigg", "vor", "mimir", "heimdall", "thor", "tyr", "forseti"])
|
|
65
|
+
.toContain(d.agent);
|
|
66
|
+
expect(d.codemodIntent).toBeNull();
|
|
67
|
+
expect(["flash", "mid", "expensive"]).toContain(d.modelTier);
|
|
68
|
+
expect(d.surfacedTags.length).toBe(1);
|
|
69
|
+
expect(d.surfacedTags[0]).toMatch(/^\[TASK_MODEL_RECOMMENDATION\]/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("unknown explicit agent falls through to q-learning", () => {
|
|
73
|
+
const mr = new ModelRouter({ seed: 42 });
|
|
74
|
+
const qr = new QLearningRouter({ seed: 42 });
|
|
75
|
+
const d = decideAgentWith(mr, qr, { task: "hello world", explicitAgent: "totally-unknown" });
|
|
76
|
+
expect(d.agent).not.toBe("totally-unknown");
|
|
77
|
+
expect(AGENT_SET.has(d.agent)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const AGENT_SET = new Set(["odin", "frigg", "vor", "mimir", "heimdall", "thor", "tyr", "forseti"]);
|
|
82
|
+
|
|
83
|
+
describe("tag formatters", () => {
|
|
84
|
+
test("codemodTag includes intent and confidence", () => {
|
|
85
|
+
const tag = codemodTag("var-to-const", 0.71);
|
|
86
|
+
expect(tag).toContain("[CODEMOD_AVAILABLE]");
|
|
87
|
+
expect(tag).toContain("var-to-const");
|
|
88
|
+
expect(tag).toMatch(/conf=0\.71/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("tierTag includes tier and confidence", () => {
|
|
92
|
+
const tag = tierTag({ tier: "flash", confidence: 0.42 });
|
|
93
|
+
expect(tag).toContain("[TASK_MODEL_RECOMMENDATION]");
|
|
94
|
+
expect(tag).toContain("flash");
|
|
95
|
+
expect(tag).toMatch(/conf=0\.42/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("getRouter() — factory + persistence", () => {
|
|
100
|
+
test("with custom routers is in-memory only", () => {
|
|
101
|
+
const mr = new ModelRouter({ seed: 1 });
|
|
102
|
+
const qr = new QLearningRouter({ seed: 1 });
|
|
103
|
+
const bundle = getRouter({ modelRouter: mr, qRouter: qr });
|
|
104
|
+
expect(bundle.modelRouter).toBe(mr);
|
|
105
|
+
expect(bundle.qRouter).toBe(qr);
|
|
106
|
+
const d = bundle.decideAgent({ task: "convert var to const" });
|
|
107
|
+
expect(d.codemodIntent).toBe("var-to-const");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("default state paths are .harness/router-state.{model,q}", () => {
|
|
111
|
+
expect(DEFAULT_MODEL_STATE_PATH).toBe(".harness/router-state.json");
|
|
112
|
+
expect(DEFAULT_Q_STATE_PATH).toBe(".harness/q-router-state.json");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("saveTo writes the bundle pointer + per-router state files", () => {
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), "bizar-router-"));
|
|
117
|
+
try {
|
|
118
|
+
const mr = new ModelRouter({ seed: 5 });
|
|
119
|
+
const qr = new QLearningRouter({ seed: 5 });
|
|
120
|
+
// Force the persistence files into a temp dir by manipulating
|
|
121
|
+
// the bundle basePath through saveTo's sibling files.
|
|
122
|
+
const bundle = getRouter({ modelRouter: mr, qRouter: qr });
|
|
123
|
+
// Override the cwd-derived basePath so the save lands in `dir`.
|
|
124
|
+
const basePath = join(dir, "router-state");
|
|
125
|
+
const modelPath = `${basePath}.model`;
|
|
126
|
+
const qPath = `${basePath}.q`;
|
|
127
|
+
mr.saveTo(modelPath);
|
|
128
|
+
qr.saveTo(qPath);
|
|
129
|
+
expect(existsSync(modelPath)).toBe(true);
|
|
130
|
+
expect(existsSync(qPath)).toBe(true);
|
|
131
|
+
const state = JSON.parse(readFileSync(modelPath, "utf-8"));
|
|
132
|
+
expect(state.version).toBe(1);
|
|
133
|
+
expect(state.priors.flash.alpha).toBe(2);
|
|
134
|
+
} finally {
|
|
135
|
+
// mkdtempSync + recursive rm
|
|
136
|
+
const { rmSync } = require("node:fs");
|
|
137
|
+
rmSync(dir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("ModelRouter.loadFrom rehydrates priors", () => {
|
|
142
|
+
const dir = mkdtempSync(join(tmpdir(), "bizar-router-"));
|
|
143
|
+
try {
|
|
144
|
+
const fp = join(dir, "router.json");
|
|
145
|
+
const original = new ModelRouter({ seed: 42 });
|
|
146
|
+
original.recordOutcome("flash", true);
|
|
147
|
+
original.recordOutcome("flash", true);
|
|
148
|
+
original.saveTo(fp);
|
|
149
|
+
|
|
150
|
+
const loaded = ModelRouter.loadFrom(fp);
|
|
151
|
+
const priors = loaded.getPriors();
|
|
152
|
+
expect(priors.flash.alpha).toBeGreaterThan(2);
|
|
153
|
+
} finally {
|
|
154
|
+
const { rmSync } = require("node:fs");
|
|
155
|
+
rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("decideAgent — codemod short-circuit covers all three intents", () => {
|
|
161
|
+
test.each([
|
|
162
|
+
["convert var to const", "var-to-const"],
|
|
163
|
+
["remove console.log calls", "remove-console"],
|
|
164
|
+
["add logging to the handler", "add-logging"],
|
|
165
|
+
])("codemod surface for: %s", (prompt, expected) => {
|
|
166
|
+
const mr = new ModelRouter({ seed: 42 });
|
|
167
|
+
const qr = new QLearningRouter({ seed: 42 });
|
|
168
|
+
const d = decideAgentWith(mr, qr, { task: prompt });
|
|
169
|
+
expect(d.codemodIntent).toBe(expected);
|
|
170
|
+
expect(d.agent).toBe("heimdall");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for codemod intent detection (F-033).
|
|
3
|
+
*
|
|
4
|
+
* 5+ positive prompts per deterministic intent (var-to-const,
|
|
5
|
+
* remove-console, add-logging) plus 5+ negatives that should NOT
|
|
6
|
+
* be codemod-eligible (the caller falls through to the bandit).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from "vitest";
|
|
10
|
+
import { detectCodemodIntent } from "../src/router/codemod-intent.js";
|
|
11
|
+
|
|
12
|
+
describe("detectCodemodIntent — var-to-const", () => {
|
|
13
|
+
test("matches canonical phrasing", () => {
|
|
14
|
+
const hit = detectCodemodIntent("convert var to const");
|
|
15
|
+
expect(hit).not.toBeNull();
|
|
16
|
+
expect(hit?.intent).toBe("var-to-const");
|
|
17
|
+
expect(hit?.confidence).toBeGreaterThan(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test.each([
|
|
21
|
+
"change var to const in this file",
|
|
22
|
+
"replace var with const",
|
|
23
|
+
"use const instead of var",
|
|
24
|
+
"var to const for all declarations",
|
|
25
|
+
"convert var declarations to const",
|
|
26
|
+
"change var declarations to const everywhere",
|
|
27
|
+
])("matches var-to-const phrasing: %s", (prompt) => {
|
|
28
|
+
const hit = detectCodemodIntent(prompt);
|
|
29
|
+
expect(hit?.intent).toBe("var-to-const");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("detectCodemodIntent — remove-console", () => {
|
|
34
|
+
test.each([
|
|
35
|
+
"remove console.log calls",
|
|
36
|
+
"delete console statements",
|
|
37
|
+
"strip console logs",
|
|
38
|
+
"clean up console",
|
|
39
|
+
"remove all debug logs",
|
|
40
|
+
"remove console.log statements everywhere",
|
|
41
|
+
])("matches remove-console phrasing: %s", (prompt) => {
|
|
42
|
+
const hit = detectCodemodIntent(prompt);
|
|
43
|
+
expect(hit?.intent).toBe("remove-console");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("detectCodemodIntent — add-logging", () => {
|
|
48
|
+
test.each([
|
|
49
|
+
"add logging to the handler",
|
|
50
|
+
"add console.log statements",
|
|
51
|
+
"add debug logs to the module",
|
|
52
|
+
"log this function for diagnostics",
|
|
53
|
+
"add trace logging",
|
|
54
|
+
"instrument with logs",
|
|
55
|
+
])("matches add-logging phrasing: %s", (prompt) => {
|
|
56
|
+
const hit = detectCodemodIntent(prompt);
|
|
57
|
+
expect(hit?.intent).toBe("add-logging");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("detectCodemodIntent — negatives", () => {
|
|
62
|
+
test.each([
|
|
63
|
+
"design a database schema for inventory",
|
|
64
|
+
"implement OAuth2 PKCE flow with refresh-token rotation",
|
|
65
|
+
"debug a memory leak in the cache",
|
|
66
|
+
"explain what this function does",
|
|
67
|
+
"refactor the http client to use async await",
|
|
68
|
+
"add type annotations to the API",
|
|
69
|
+
"wrap the parser in try/catch",
|
|
70
|
+
])("rejects non-codemod prompt: %s", (prompt) => {
|
|
71
|
+
expect(detectCodemodIntent(prompt)).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("returns null for empty input", () => {
|
|
75
|
+
expect(detectCodemodIntent("")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("returns null for non-string input", () => {
|
|
79
|
+
expect(detectCodemodIntent(null)).toBeNull();
|
|
80
|
+
expect(detectCodemodIntent(undefined)).toBeNull();
|
|
81
|
+
expect(detectCodemodIntent(42)).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("detectCodemodIntent — confidence ordering", () => {
|
|
86
|
+
test("multi-match returns the highest-confidence intent", () => {
|
|
87
|
+
const hit = detectCodemodIntent("convert var to const, replace var with const, change var declarations to const");
|
|
88
|
+
expect(hit?.intent).toBe("var-to-const");
|
|
89
|
+
expect(hit?.confidence).toBeGreaterThan(0.4);
|
|
90
|
+
});
|
|
91
|
+
});
|