@a5c-ai/krate 5.0.1-staging.00fa5317c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +183 -0
  3. package/bin/krate-demo.mjs +23 -0
  4. package/bin/krate-server.mjs +14 -0
  5. package/dist/krate-controller-ui.json +3205 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +3125 -0
  8. package/dist/krate-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/components/control-plane.md +78 -0
  60. package/docs/components/data-plane.md +69 -0
  61. package/docs/components/hooks-events.md +67 -0
  62. package/docs/components/identity-rbac-policy.md +73 -0
  63. package/docs/components/kubevela-oam.md +70 -0
  64. package/docs/components/operations-publishing.md +81 -0
  65. package/docs/components/runners-ci.md +66 -0
  66. package/docs/components/web-ui.md +94 -0
  67. package/docs/external/README.md +47 -0
  68. package/docs/external/bidirectional-sync-design.md +134 -0
  69. package/docs/external/cicd-interface.md +64 -0
  70. package/docs/external/external-backend-controllers.md +170 -0
  71. package/docs/external/external-backend-crds.md +234 -0
  72. package/docs/external/external-backend-ui-spec.md +151 -0
  73. package/docs/external/external-backend-ux-flows.md +115 -0
  74. package/docs/external/external-object-mapping.md +125 -0
  75. package/docs/external/git-forge-interface.md +68 -0
  76. package/docs/external/github-integration-design.md +151 -0
  77. package/docs/external/issue-tracking-interface.md +66 -0
  78. package/docs/external/provider-capability-manifests.md +204 -0
  79. package/docs/external/provider-catalog.md +139 -0
  80. package/docs/external/provider-rollout-testing.md +78 -0
  81. package/docs/external/research-results.md +48 -0
  82. package/docs/external/security-auth-permissions.md +81 -0
  83. package/docs/external/sync-state-machines.md +108 -0
  84. package/docs/external/unified-external-backend-model.md +107 -0
  85. package/docs/external/user-facing-changes.md +67 -0
  86. package/docs/gaps.md +161 -0
  87. package/docs/install.md +94 -0
  88. package/docs/krate-design.md +334 -0
  89. package/docs/local-minikube.md +55 -0
  90. package/docs/ontology/README.md +32 -0
  91. package/docs/ontology/bounded-contexts.md +29 -0
  92. package/docs/ontology/events-and-hooks.md +32 -0
  93. package/docs/ontology/oam-kubevela.md +32 -0
  94. package/docs/ontology/operations-and-release.md +25 -0
  95. package/docs/ontology/personas-and-actors.md +32 -0
  96. package/docs/ontology/policies-and-invariants.md +33 -0
  97. package/docs/ontology/problem-space.md +30 -0
  98. package/docs/ontology/resource-contracts.md +40 -0
  99. package/docs/ontology/resource-taxonomy.md +42 -0
  100. package/docs/ontology/runners-and-ci.md +29 -0
  101. package/docs/ontology/solution-space.md +24 -0
  102. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  103. package/docs/ontology/validation-matrix.md +24 -0
  104. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  105. package/docs/ontology/workflows.md +39 -0
  106. package/docs/ontology/world.md +35 -0
  107. package/docs/openapi.yaml +1275 -0
  108. package/docs/product-requirements.md +62 -0
  109. package/docs/roadmap-mvp.md +87 -0
  110. package/docs/system-requirements.md +90 -0
  111. package/docs/tests/README.md +53 -0
  112. package/docs/tests/agent-qa-plan.md +63 -0
  113. package/docs/tests/browser-ui-tests.md +62 -0
  114. package/docs/tests/ci-quality-gates.md +48 -0
  115. package/docs/tests/coverage-model.md +64 -0
  116. package/docs/tests/e2e-scenario-tests.md +53 -0
  117. package/docs/tests/fixtures-test-data.md +63 -0
  118. package/docs/tests/observability-reliability-tests.md +54 -0
  119. package/docs/tests/product-test-matrix.md +145 -0
  120. package/docs/tests/qa-adoption-roadmap.md +130 -0
  121. package/docs/tests/qa-automation-plan.md +101 -0
  122. package/docs/tests/security-compliance-tests.md +57 -0
  123. package/docs/tests/test-framework-tools.md +88 -0
  124. package/docs/tests/test-suite-layout.md +121 -0
  125. package/docs/tests/unit-integration-tests.md +48 -0
  126. package/docs/todo-kyverno +714 -0
  127. package/docs/todos.md +4 -0
  128. package/docs/user-stories.md +78 -0
  129. package/examples/minikube-demo.yaml +190 -0
  130. package/examples/oam-application.yaml +23 -0
  131. package/examples/policy-kyverno-pr-title.yaml +18 -0
  132. package/package.json +63 -0
  133. package/scripts/build.mjs +29 -0
  134. package/scripts/setup-minikube.mjs +65 -0
  135. package/scripts/smoke.mjs +37 -0
  136. package/scripts/validate-doc-coverage.mjs +152 -0
  137. package/scripts/validate-package.mjs +93 -0
  138. package/scripts/validate-ui.mjs +278 -0
  139. package/src/agent-adapter-controller.js +169 -0
  140. package/src/agent-approval-controller.js +170 -0
  141. package/src/agent-context-bundles.js +242 -0
  142. package/src/agent-dispatch-controller.js +209 -0
  143. package/src/agent-gateway-config-controller.js +147 -0
  144. package/src/agent-memory-controller.js +357 -0
  145. package/src/agent-memory-import.js +327 -0
  146. package/src/agent-memory-query.js +292 -0
  147. package/src/agent-memory-repository-source-controller.js +255 -0
  148. package/src/agent-mux-client.js +280 -0
  149. package/src/agent-permission-review.js +250 -0
  150. package/src/agent-project-controller.js +117 -0
  151. package/src/agent-provider-config-controller.js +150 -0
  152. package/src/agent-secret-config-grant-controller.js +282 -0
  153. package/src/agent-session-transcript-controller.js +189 -0
  154. package/src/agent-stack-controller.js +347 -0
  155. package/src/agent-subagent-controller.js +160 -0
  156. package/src/agent-transport-binding-controller.js +121 -0
  157. package/src/agent-trigger-controller.js +381 -0
  158. package/src/agent-workspace-controller.js +702 -0
  159. package/src/agent-writeback-controller.js +302 -0
  160. package/src/api-controller.js +541 -0
  161. package/src/argocd-gitops.js +43 -0
  162. package/src/async-controller.js +207 -0
  163. package/src/audit-controller.js +191 -0
  164. package/src/auth.js +307 -0
  165. package/src/component-catalog.js +41 -0
  166. package/src/control-plane.js +136 -0
  167. package/src/controller-client.js +72 -0
  168. package/src/controller-ui.js +617 -0
  169. package/src/data-plane.js +179 -0
  170. package/src/event-bus.js +61 -0
  171. package/src/external/conflict-controller.js +225 -0
  172. package/src/external/github/auth.js +96 -0
  173. package/src/external/github/cicd.js +180 -0
  174. package/src/external/github/git-forge.js +240 -0
  175. package/src/external/github/index.js +144 -0
  176. package/src/external/github/issue-tracking.js +163 -0
  177. package/src/external/provider-adapter.js +161 -0
  178. package/src/external/provider-resource-factory.js +161 -0
  179. package/src/external/sync-controller.js +235 -0
  180. package/src/external/webhook-controller.js +144 -0
  181. package/src/external/write-controller.js +283 -0
  182. package/src/gitea-backend.js +131 -0
  183. package/src/gitea-service.js +173 -0
  184. package/src/handoff.js +98 -0
  185. package/src/hooks-events.js +63 -0
  186. package/src/http-server.js +377 -0
  187. package/src/identity-policy.js +86 -0
  188. package/src/index.js +57 -0
  189. package/src/kubernetes-controller-async.js +511 -0
  190. package/src/kubernetes-controller.js +878 -0
  191. package/src/kubernetes-resource-gateway.js +48 -0
  192. package/src/notification-controller.js +178 -0
  193. package/src/operations.js +112 -0
  194. package/src/org-scoping.js +5 -0
  195. package/src/resource-model.js +221 -0
  196. package/src/runner-controller.js +272 -0
  197. package/src/runners-ci.js +48 -0
  198. package/src/runtime.js +196 -0
  199. package/src/snapshot-cache.js +157 -0
  200. package/src/web-ui.js +40 -0
  201. package/tests/agent-adapter-controller.test.js +361 -0
  202. package/tests/agent-approval-controller.test.js +173 -0
  203. package/tests/agent-context-bundles.test.js +278 -0
  204. package/tests/agent-dispatch-controller.test.js +315 -0
  205. package/tests/agent-gateway-config-controller.test.js +386 -0
  206. package/tests/agent-memory-controller.test.js +308 -0
  207. package/tests/agent-memory-import-snapshot.test.js +477 -0
  208. package/tests/agent-memory-query.test.js +404 -0
  209. package/tests/agent-memory-repository-source.test.js +514 -0
  210. package/tests/agent-mux-client.test.js +204 -0
  211. package/tests/agent-permission-review-v2.test.js +317 -0
  212. package/tests/agent-permission-review.test.js +209 -0
  213. package/tests/agent-project-controller.test.js +302 -0
  214. package/tests/agent-provider-config-controller.test.js +376 -0
  215. package/tests/agent-resources.test.js +228 -0
  216. package/tests/agent-secret-config-grant.test.js +231 -0
  217. package/tests/agent-session-transcript-controller.test.js +499 -0
  218. package/tests/agent-stack-controller.test.js +221 -0
  219. package/tests/agent-subagent-controller.test.js +201 -0
  220. package/tests/agent-transport-binding-controller.test.js +294 -0
  221. package/tests/agent-trigger-controller.test.js +211 -0
  222. package/tests/agent-trigger-routes.test.js +190 -0
  223. package/tests/agent-trigger-sources.test.js +245 -0
  224. package/tests/agent-workspace-controller.test.js +181 -0
  225. package/tests/agent-writeback.test.js +292 -0
  226. package/tests/approval-persistence.test.js +171 -0
  227. package/tests/async-controller.test.js +252 -0
  228. package/tests/audit-controller.test.js +227 -0
  229. package/tests/codespace-controller.test.js +318 -0
  230. package/tests/deployment.test.js +407 -0
  231. package/tests/e2e/lifecycle.test.js +117 -0
  232. package/tests/event-bus-integration.test.js +190 -0
  233. package/tests/external-github-forge.test.js +560 -0
  234. package/tests/external-github-issues-cicd.test.js +520 -0
  235. package/tests/external-integration.test.js +470 -0
  236. package/tests/external-persistence.test.js +340 -0
  237. package/tests/external-provider-adapter.test.js +365 -0
  238. package/tests/external-resource-model.test.js +215 -0
  239. package/tests/external-webhook-sync.test.js +287 -0
  240. package/tests/external-write-conflict.test.js +353 -0
  241. package/tests/gitea-service.test.js +253 -0
  242. package/tests/health-check-real.test.js +165 -0
  243. package/tests/integration/full-flow.test.js +266 -0
  244. package/tests/krate.test.js +756 -0
  245. package/tests/memory-search-wiring.test.js +270 -0
  246. package/tests/notification-controller.test.js +196 -0
  247. package/tests/notification-integration.test.js +179 -0
  248. package/tests/org-scoping.test.js +687 -0
  249. package/tests/runner-controller.test.js +327 -0
  250. package/tests/runner-integration.test.js +231 -0
  251. package/tests/session-cookie-hmac.test.js +151 -0
  252. package/tests/snapshot-performance.test.js +247 -0
  253. package/tests/sse-events.test.js +107 -0
  254. package/tests/webhook-trigger.test.js +198 -0
  255. package/tests/workspace-volumes.test.js +312 -0
  256. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Tests for agent-memory-query.js — Memory Query Engine (Slice 2.3b)
3
+ *
4
+ * Covers:
5
+ * - queryGraph: nodeKind filtering, edge following (depth 1), relevance scoring, empty results, missing query
6
+ * - queryGrep: matching excerpts with path + lineNumber, highlight, context lines, empty results, empty query
7
+ * - queryMemory: combined mode, graph-only, grep-only, stats, invalid mode
8
+ */
9
+
10
+ import assert from 'node:assert/strict';
11
+ import test from 'node:test';
12
+ import { queryGraph, queryGrep, queryMemory } from '../src/agent-memory-query.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Fixtures
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function makeRecords() {
19
+ return [
20
+ {
21
+ id: 'service/auth-api',
22
+ nodeKind: 'Service',
23
+ attributes: { name: 'auth-api', language: 'typescript', team: 'platform' },
24
+ edges: [
25
+ { target: 'service/user-db', kind: 'depends-on' },
26
+ { target: 'team/platform', kind: 'owned-by' },
27
+ ],
28
+ },
29
+ {
30
+ id: 'service/user-db',
31
+ nodeKind: 'Service',
32
+ attributes: { name: 'user-db', language: 'go', team: 'data' },
33
+ edges: [
34
+ { target: 'infra/postgres-cluster', kind: 'depends-on' },
35
+ ],
36
+ },
37
+ {
38
+ id: 'team/platform',
39
+ nodeKind: 'Team',
40
+ attributes: { name: 'platform', lead: 'alice' },
41
+ edges: [],
42
+ },
43
+ {
44
+ id: 'infra/postgres-cluster',
45
+ nodeKind: 'Infrastructure',
46
+ attributes: { name: 'postgres-cluster', provider: 'aws' },
47
+ edges: [],
48
+ },
49
+ {
50
+ id: 'runbook/deploy-auth',
51
+ nodeKind: 'Runbook',
52
+ attributes: { name: 'deploy-auth', service: 'auth-api' },
53
+ edges: [
54
+ { target: 'service/auth-api', kind: 'references' },
55
+ ],
56
+ },
57
+ ];
58
+ }
59
+
60
+ function makeDocuments() {
61
+ return [
62
+ {
63
+ path: 'docs/architecture.md',
64
+ content: [
65
+ 'The auth-api service handles authentication.',
66
+ 'It uses JWT tokens for session management.',
67
+ 'The service connects to user-db for persistence.',
68
+ ].join('\n'),
69
+ },
70
+ {
71
+ path: 'docs/runbooks/deploy.md',
72
+ content: [
73
+ 'Step 1: Run the deploy script.',
74
+ 'Step 2: Verify health checks.',
75
+ 'Step 3: Monitor error rates.',
76
+ ].join('\n'),
77
+ },
78
+ {
79
+ path: 'src/auth/handler.ts',
80
+ content: [
81
+ 'export function handleAuth(req) {',
82
+ ' const token = req.headers.authorization;',
83
+ ' return validateToken(token);',
84
+ '}',
85
+ ].join('\n'),
86
+ },
87
+ {
88
+ path: 'configs/prod.yaml',
89
+ content: [
90
+ 'database:',
91
+ ' host: prod-db.internal',
92
+ ' port: 5432',
93
+ ].join('\n'),
94
+ },
95
+ ];
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // queryGraph tests
100
+ // ---------------------------------------------------------------------------
101
+
102
+ test('queryGraph returns matching records filtered by nodeKind', () => {
103
+ const records = makeRecords();
104
+ const result = queryGraph({ records, query: 'api', kinds: ['Service'] });
105
+
106
+ assert.ok(result.matches.length > 0, 'Should have matches');
107
+ assert.ok(result.matches.every(m => m.record.nodeKind === 'Service'), 'All matches must be Service');
108
+ const authMatch = result.matches.find(m => m.record.id === 'service/auth-api');
109
+ assert.ok(authMatch, 'Should include auth-api');
110
+ });
111
+
112
+ test('queryGraph returns no non-Service records when filtering by Service', () => {
113
+ const records = makeRecords();
114
+ // 'platform' appears in both Service and Team records
115
+ const result = queryGraph({ records, query: 'platform', kinds: ['Service'] });
116
+
117
+ const teamMatch = result.matches.find(m => m.record.nodeKind === 'Team');
118
+ assert.equal(teamMatch, undefined, 'Should not include Team records when filtering by Service');
119
+ });
120
+
121
+ test('queryGraph follows edges to depth 1', () => {
122
+ const records = makeRecords();
123
+ const result = queryGraph({ records, query: 'auth-api', kinds: [], depth: 1 });
124
+
125
+ const authMatch = result.matches.find(m => m.record.id === 'service/auth-api');
126
+ assert.ok(authMatch, 'Should find auth-api record');
127
+
128
+ const edgeTargets = authMatch.edges.map(e => e.target);
129
+ assert.ok(edgeTargets.includes('service/user-db'), 'Depth-1 edges should include user-db');
130
+ assert.ok(edgeTargets.includes('team/platform'), 'Depth-1 edges should include team/platform');
131
+ // user-db's edge (postgres-cluster) should NOT appear at depth 1
132
+ assert.ok(!edgeTargets.includes('infra/postgres-cluster'), 'Depth-1 should NOT include postgres-cluster');
133
+ });
134
+
135
+ test('queryGraph follows edges to depth 2 (transitive)', () => {
136
+ const records = makeRecords();
137
+ const result = queryGraph({ records, query: 'auth-api', kinds: [], depth: 2 });
138
+
139
+ const authMatch = result.matches.find(m => m.record.id === 'service/auth-api');
140
+ assert.ok(authMatch, 'Should find auth-api record');
141
+
142
+ const edgeTargets = authMatch.edges.map(e => e.target);
143
+ // At depth 2, user-db's dependency (postgres-cluster) should be reachable
144
+ assert.ok(edgeTargets.includes('infra/postgres-cluster'), 'Depth-2 should include postgres-cluster');
145
+ });
146
+
147
+ test('queryGraph returns relevance scores: id match scores higher than attribute match', () => {
148
+ const records = makeRecords();
149
+ // 'auth-api' appears in the id of service/auth-api (score 2) and in the attributes of runbook/deploy-auth (score 1)
150
+ const result = queryGraph({ records, query: 'auth-api', kinds: [] });
151
+
152
+ const authApiMatch = result.matches.find(m => m.record.id === 'service/auth-api');
153
+ const runbookMatch = result.matches.find(m => m.record.id === 'runbook/deploy-auth');
154
+
155
+ assert.ok(authApiMatch, 'auth-api record should be in matches');
156
+ assert.ok(runbookMatch, 'runbook record should be in matches (attribute match)');
157
+ assert.equal(authApiMatch.score, 2, 'ID match should have score 2');
158
+ assert.equal(runbookMatch.score, 1, 'Attribute match should have score 1');
159
+ // Results should be sorted descending by score
160
+ const scores = result.matches.map(m => m.score);
161
+ for (let i = 1; i < scores.length; i++) {
162
+ assert.ok(scores[i] <= scores[i - 1], 'Results should be sorted by descending score');
163
+ }
164
+ });
165
+
166
+ test('queryGraph returns empty results for no matches', () => {
167
+ const records = makeRecords();
168
+ const result = queryGraph({ records, query: 'zzz-no-match-xyz', kinds: [] });
169
+
170
+ assert.equal(result.totalMatches, 0);
171
+ assert.deepEqual(result.matches, []);
172
+ });
173
+
174
+ test('queryGraph returns empty results when nodeKind filter matches nothing', () => {
175
+ const records = makeRecords();
176
+ const result = queryGraph({ records, query: 'auth', kinds: ['NonExistentKind'] });
177
+
178
+ assert.equal(result.totalMatches, 0);
179
+ assert.deepEqual(result.matches, []);
180
+ });
181
+
182
+ test('queryGraph rejects missing query text', () => {
183
+ const records = makeRecords();
184
+ assert.throws(
185
+ () => queryGraph({ records }),
186
+ /query text is required/,
187
+ 'Should throw when query is missing'
188
+ );
189
+ });
190
+
191
+ test('queryGraph rejects empty string query', () => {
192
+ const records = makeRecords();
193
+ assert.throws(
194
+ () => queryGraph({ records, query: ' ' }),
195
+ /non-empty string/,
196
+ 'Should throw when query is whitespace-only'
197
+ );
198
+ });
199
+
200
+ test('queryGraph uses flat edges array to supplement per-record edges', () => {
201
+ const records = [
202
+ { id: 'node/a', nodeKind: 'Service', attributes: { name: 'node-a' }, edges: [] },
203
+ { id: 'node/b', nodeKind: 'Service', attributes: { name: 'node-b' }, edges: [] },
204
+ ];
205
+ const flatEdges = [{ source: 'node/a', target: 'node/b', kind: 'calls' }];
206
+
207
+ const result = queryGraph({ records, edges: flatEdges, query: 'node-a', kinds: [], depth: 1 });
208
+ const matchA = result.matches.find(m => m.record.id === 'node/a');
209
+ assert.ok(matchA, 'Should match node/a');
210
+ const edgeTargets = matchA.edges.map(e => e.target);
211
+ assert.ok(edgeTargets.includes('node/b'), 'Flat edge should be followed');
212
+ });
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // queryGrep tests
216
+ // ---------------------------------------------------------------------------
217
+
218
+ test('queryGrep returns matching excerpts with file path and line number', () => {
219
+ const documents = makeDocuments();
220
+ const result = queryGrep({ documents, query: 'auth-api' });
221
+
222
+ assert.ok(result.totalMatches > 0, 'Should find matches for auth-api');
223
+ for (const excerpt of result.excerpts) {
224
+ assert.ok(typeof excerpt.path === 'string' && excerpt.path.length > 0, 'Each excerpt must have a path');
225
+ assert.ok(typeof excerpt.lineNumber === 'number' && excerpt.lineNumber >= 1, 'Each excerpt must have a lineNumber >= 1');
226
+ }
227
+ });
228
+
229
+ test('queryGrep highlights matched text with ** markers', () => {
230
+ const documents = makeDocuments();
231
+ const result = queryGrep({ documents, query: 'auth-api' });
232
+
233
+ assert.ok(result.excerpts.length > 0, 'Should have excerpts');
234
+ const withHighlight = result.excerpts.filter(e => e.highlighted.includes('**auth-api**'));
235
+ assert.ok(withHighlight.length > 0, 'At least one excerpt should have highlighted match with ** markers');
236
+ });
237
+
238
+ test('queryGrep returns context lines around matches', () => {
239
+ const documents = makeDocuments();
240
+ const result = queryGrep({ documents, query: 'JWT', context: 1 });
241
+
242
+ assert.ok(result.excerpts.length > 0, 'Should find JWT');
243
+ // "JWT tokens" is on line 2 of docs/architecture.md; context of 1 means lines 1–3
244
+ const archExcerpt = result.excerpts.find(e => e.path === 'docs/architecture.md');
245
+ assert.ok(archExcerpt, 'Should have a match in architecture.md');
246
+ assert.ok(archExcerpt.context.includes('auth-api') || archExcerpt.context.includes('user-db'),
247
+ 'Context should include adjacent lines');
248
+ });
249
+
250
+ test('queryGrep returns empty for no matches', () => {
251
+ const documents = makeDocuments();
252
+ const result = queryGrep({ documents, query: 'zzz-no-such-term-xyz' });
253
+
254
+ assert.equal(result.totalMatches, 0);
255
+ assert.deepEqual(result.excerpts, []);
256
+ });
257
+
258
+ test('queryGrep rejects empty query', () => {
259
+ const documents = makeDocuments();
260
+ assert.throws(
261
+ () => queryGrep({ documents, query: '' }),
262
+ /non-empty string/,
263
+ 'Should throw for empty query string'
264
+ );
265
+ });
266
+
267
+ test('queryGrep rejects missing query', () => {
268
+ const documents = makeDocuments();
269
+ assert.throws(
270
+ () => queryGrep({ documents }),
271
+ /query text is required/,
272
+ 'Should throw when query is missing'
273
+ );
274
+ });
275
+
276
+ test('queryGrep respects maxMatches cap', () => {
277
+ const documents = makeDocuments();
278
+ const result = queryGrep({ documents, query: 'e', maxMatches: 3 });
279
+
280
+ assert.ok(result.excerpts.length <= 3, 'Should not exceed maxMatches');
281
+ assert.ok(result.totalMatches <= 3, 'totalMatches should not exceed maxMatches');
282
+ });
283
+
284
+ test('queryGrep filters documents by path glob', () => {
285
+ const documents = makeDocuments();
286
+ const result = queryGrep({ documents, query: 'auth', paths: ['docs/*'] });
287
+
288
+ assert.ok(result.totalMatches > 0, 'Should find auth in docs/');
289
+ for (const excerpt of result.excerpts) {
290
+ assert.ok(excerpt.path.startsWith('docs/'), 'All excerpts must come from docs/ paths');
291
+ }
292
+ // src/auth/handler.ts should NOT appear because paths filter is docs/*
293
+ const srcMatch = result.excerpts.find(e => e.path.startsWith('src/'));
294
+ assert.equal(srcMatch, undefined, 'src/ files should be excluded by path filter');
295
+ });
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // queryMemory combined tests
299
+ // ---------------------------------------------------------------------------
300
+
301
+ test('queryMemory with mode graph-and-grep returns both result types', () => {
302
+ const records = makeRecords();
303
+ const documents = makeDocuments();
304
+ const result = queryMemory({ query: 'auth-api', mode: 'graph-and-grep', records, documents });
305
+
306
+ assert.ok(result.graph !== null, 'graph results should be present');
307
+ assert.ok(result.grep !== null, 'grep results should be present');
308
+ assert.ok(result.graph.totalMatches >= 0, 'graph.totalMatches should be a number');
309
+ assert.ok(result.grep.totalMatches >= 0, 'grep.totalMatches should be a number');
310
+ });
311
+
312
+ test('queryMemory with mode graph-only returns only graph results', () => {
313
+ const records = makeRecords();
314
+ const result = queryMemory({ query: 'auth-api', mode: 'graph-only', records });
315
+
316
+ assert.ok(result.graph !== null, 'graph results should be present');
317
+ assert.equal(result.grep, null, 'grep results should be null in graph-only mode');
318
+ });
319
+
320
+ test('queryMemory with mode grep-only returns only grep results', () => {
321
+ const documents = makeDocuments();
322
+ const result = queryMemory({ query: 'auth-api', mode: 'grep-only', documents });
323
+
324
+ assert.equal(result.graph, null, 'graph results should be null in grep-only mode');
325
+ assert.ok(result.grep !== null, 'grep results should be present');
326
+ });
327
+
328
+ test('queryMemory returns stats with totalMatches, graphCount, grepCount', () => {
329
+ const records = makeRecords();
330
+ const documents = makeDocuments();
331
+ const result = queryMemory({ query: 'auth-api', mode: 'graph-and-grep', records, documents });
332
+
333
+ assert.ok('stats' in result, 'result should have stats property');
334
+ const { stats } = result;
335
+ assert.ok(typeof stats.totalMatches === 'number', 'stats.totalMatches must be a number');
336
+ assert.ok(typeof stats.graphCount === 'number', 'stats.graphCount must be a number');
337
+ assert.ok(typeof stats.grepCount === 'number', 'stats.grepCount must be a number');
338
+ assert.equal(stats.totalMatches, stats.graphCount + stats.grepCount, 'totalMatches = graphCount + grepCount');
339
+ assert.equal(stats.mode, 'graph-and-grep', 'stats.mode should reflect the requested mode');
340
+ });
341
+
342
+ test('queryMemory returns zero counts for grep in graph-only mode', () => {
343
+ const records = makeRecords();
344
+ const result = queryMemory({ query: 'auth-api', mode: 'graph-only', records });
345
+
346
+ assert.equal(result.stats.grepCount, 0, 'grepCount should be 0 in graph-only mode');
347
+ assert.equal(result.stats.totalMatches, result.stats.graphCount, 'totalMatches equals graphCount in graph-only');
348
+ });
349
+
350
+ test('queryMemory rejects invalid mode', () => {
351
+ assert.throws(
352
+ () => queryMemory({ query: 'auth', mode: 'invalid-mode' }),
353
+ /invalid mode/,
354
+ 'Should throw for unrecognized mode'
355
+ );
356
+ });
357
+
358
+ test('queryMemory rejects missing query', () => {
359
+ assert.throws(
360
+ () => queryMemory({ mode: 'graph-only', records: [] }),
361
+ /query text is required/,
362
+ 'Should throw when query is missing'
363
+ );
364
+ });
365
+
366
+ test('queryMemory default mode is graph-and-grep', () => {
367
+ const records = makeRecords();
368
+ const documents = makeDocuments();
369
+ // No mode specified — should default to graph-and-grep
370
+ const result = queryMemory({ query: 'auth-api', records, documents });
371
+
372
+ assert.equal(result.stats.mode, 'graph-and-grep', 'Default mode should be graph-and-grep');
373
+ assert.ok(result.graph !== null, 'graph results should be present by default');
374
+ assert.ok(result.grep !== null, 'grep results should be present by default');
375
+ });
376
+
377
+ test('queryMemory passes graphOptions through to queryGraph', () => {
378
+ const records = makeRecords();
379
+ // Only 'Team' nodeKind, querying 'platform' which matches team/platform by id
380
+ const result = queryMemory({
381
+ query: 'platform',
382
+ mode: 'graph-only',
383
+ records,
384
+ graphOptions: { kinds: ['Team'] },
385
+ });
386
+
387
+ assert.ok(result.graph.totalMatches >= 1, 'Should find platform team');
388
+ assert.ok(result.graph.matches.every(m => m.record.nodeKind === 'Team'), 'All graph results should be Team');
389
+ });
390
+
391
+ test('queryMemory passes grepOptions through to queryGrep', () => {
392
+ const documents = makeDocuments();
393
+ const result = queryMemory({
394
+ query: 'auth',
395
+ mode: 'grep-only',
396
+ documents,
397
+ grepOptions: { paths: ['src/*'], maxMatches: 5 },
398
+ });
399
+
400
+ assert.ok(result.grep !== null, 'grep results should be present');
401
+ for (const excerpt of result.grep.excerpts) {
402
+ assert.ok(excerpt.path.startsWith('src/'), 'All grep results should be in src/ due to path filter');
403
+ }
404
+ });