@nomos-arc/arc 0.1.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/settings.local.json +10 -0
- package/.nomos-config.json +5 -0
- package/CLAUDE.md +108 -0
- package/LICENSE +190 -0
- package/README.md +569 -0
- package/dist/cli.js +21120 -0
- package/docs/auth/googel_plan.yaml +1093 -0
- package/docs/auth/google_task.md +235 -0
- package/docs/auth/hardened_blueprint.yaml +1658 -0
- package/docs/auth/red_team_report.yaml +336 -0
- package/docs/auth/session_state.yaml +162 -0
- package/docs/certificate/cer_enhance_plan.md +605 -0
- package/docs/certificate/certificate_report.md +338 -0
- package/docs/dev_overview.md +419 -0
- package/docs/feature_assessment.md +156 -0
- package/docs/how_it_works.md +78 -0
- package/docs/infrastructure/map.md +867 -0
- package/docs/init/master_plan.md +3581 -0
- package/docs/init/red_team_report.md +215 -0
- package/docs/init/report_phase_1a.md +304 -0
- package/docs/integrity-gate/enhance_drift.md +703 -0
- package/docs/integrity-gate/overview.md +108 -0
- package/docs/management/manger-task.md +99 -0
- package/docs/management/scafffold.md +76 -0
- package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
- package/docs/map/RED_TEAM_REPORT.md +159 -0
- package/docs/map/map_task.md +147 -0
- package/docs/map/semantic_graph_task.md +792 -0
- package/docs/map/semantic_master_plan.md +705 -0
- package/docs/phase7/TEAM_RED.md +249 -0
- package/docs/phase7/plan.md +1682 -0
- package/docs/phase7/task.md +275 -0
- package/docs/prompts/USAGE.md +312 -0
- package/docs/prompts/architect.md +165 -0
- package/docs/prompts/executer.md +190 -0
- package/docs/prompts/hardener.md +190 -0
- package/docs/prompts/red_team.md +146 -0
- package/docs/verification/goveranance-overview.md +396 -0
- package/docs/verification/governance-overview.md +245 -0
- package/docs/verification/verification-arc-ar.md +560 -0
- package/docs/verification/verification-architecture.md +560 -0
- package/docs/very_next.md +52 -0
- package/docs/whitepaper.md +89 -0
- package/overview.md +1469 -0
- package/package.json +63 -0
- package/src/adapters/__tests__/git.test.ts +296 -0
- package/src/adapters/__tests__/stdio.test.ts +70 -0
- package/src/adapters/git.ts +226 -0
- package/src/adapters/pty.ts +159 -0
- package/src/adapters/stdio.ts +113 -0
- package/src/cli.ts +83 -0
- package/src/commands/apply.ts +47 -0
- package/src/commands/auth.ts +301 -0
- package/src/commands/certificate.ts +89 -0
- package/src/commands/discard.ts +24 -0
- package/src/commands/drift.ts +116 -0
- package/src/commands/index.ts +78 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/list.ts +75 -0
- package/src/commands/map.ts +55 -0
- package/src/commands/plan.ts +30 -0
- package/src/commands/review.ts +58 -0
- package/src/commands/run.ts +63 -0
- package/src/commands/search.ts +147 -0
- package/src/commands/show.ts +63 -0
- package/src/commands/status.ts +59 -0
- package/src/core/__tests__/budget.test.ts +213 -0
- package/src/core/__tests__/certificate.test.ts +385 -0
- package/src/core/__tests__/config.test.ts +191 -0
- package/src/core/__tests__/preflight.test.ts +24 -0
- package/src/core/__tests__/prompt.test.ts +358 -0
- package/src/core/__tests__/review.test.ts +161 -0
- package/src/core/__tests__/state.test.ts +362 -0
- package/src/core/auth/__tests__/manager.test.ts +166 -0
- package/src/core/auth/__tests__/server.test.ts +220 -0
- package/src/core/auth/gcp-projects.ts +160 -0
- package/src/core/auth/manager.ts +114 -0
- package/src/core/auth/server.ts +141 -0
- package/src/core/budget.ts +119 -0
- package/src/core/certificate.ts +502 -0
- package/src/core/config.ts +212 -0
- package/src/core/errors.ts +54 -0
- package/src/core/factory.ts +49 -0
- package/src/core/graph/__tests__/builder.test.ts +272 -0
- package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
- package/src/core/graph/__tests__/enricher.test.ts +299 -0
- package/src/core/graph/__tests__/parser.test.ts +200 -0
- package/src/core/graph/__tests__/pipeline.test.ts +202 -0
- package/src/core/graph/__tests__/renderer.test.ts +128 -0
- package/src/core/graph/__tests__/resolver.test.ts +185 -0
- package/src/core/graph/__tests__/scanner.test.ts +231 -0
- package/src/core/graph/__tests__/show.test.ts +134 -0
- package/src/core/graph/builder.ts +303 -0
- package/src/core/graph/constraints.ts +94 -0
- package/src/core/graph/contract-writer.ts +93 -0
- package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
- package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
- package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
- package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
- package/src/core/graph/drift/classifier.ts +165 -0
- package/src/core/graph/drift/comparator.ts +205 -0
- package/src/core/graph/drift/reporter.ts +77 -0
- package/src/core/graph/enricher.ts +251 -0
- package/src/core/graph/grammar-paths.ts +30 -0
- package/src/core/graph/html-template.ts +493 -0
- package/src/core/graph/map-schema.ts +137 -0
- package/src/core/graph/parser.ts +336 -0
- package/src/core/graph/pipeline.ts +209 -0
- package/src/core/graph/renderer.ts +92 -0
- package/src/core/graph/resolver.ts +195 -0
- package/src/core/graph/scanner.ts +145 -0
- package/src/core/logger.ts +46 -0
- package/src/core/orchestrator.ts +792 -0
- package/src/core/plan-file-manager.ts +66 -0
- package/src/core/preflight.ts +64 -0
- package/src/core/prompt.ts +173 -0
- package/src/core/review.ts +95 -0
- package/src/core/state.ts +294 -0
- package/src/core/worktree-coordinator.ts +77 -0
- package/src/search/__tests__/chunk-extractor.test.ts +339 -0
- package/src/search/__tests__/embedder-auth.test.ts +124 -0
- package/src/search/__tests__/embedder.test.ts +267 -0
- package/src/search/__tests__/graph-enricher.test.ts +178 -0
- package/src/search/__tests__/indexer.test.ts +518 -0
- package/src/search/__tests__/integration.test.ts +649 -0
- package/src/search/__tests__/query-engine.test.ts +334 -0
- package/src/search/__tests__/similarity.test.ts +78 -0
- package/src/search/__tests__/vector-store.test.ts +281 -0
- package/src/search/chunk-extractor.ts +167 -0
- package/src/search/embedder.ts +209 -0
- package/src/search/graph-enricher.ts +95 -0
- package/src/search/indexer.ts +483 -0
- package/src/search/lexical-searcher.ts +190 -0
- package/src/search/query-engine.ts +225 -0
- package/src/search/vector-store.ts +311 -0
- package/src/types/index.ts +572 -0
- package/src/utils/__tests__/ansi.test.ts +54 -0
- package/src/utils/__tests__/frontmatter.test.ts +79 -0
- package/src/utils/__tests__/sanitize.test.ts +229 -0
- package/src/utils/ansi.ts +19 -0
- package/src/utils/context.ts +44 -0
- package/src/utils/frontmatter.ts +27 -0
- package/src/utils/sanitize.ts +78 -0
- package/test/e2e/lifecycle.test.ts +330 -0
- package/test/fixtures/mock-planner-hang.ts +5 -0
- package/test/fixtures/mock-planner.ts +26 -0
- package/test/fixtures/mock-reviewer-bad.ts +8 -0
- package/test/fixtures/mock-reviewer-retry.ts +34 -0
- package/test/fixtures/mock-reviewer.ts +18 -0
- package/test/fixtures/sample-project/src/circular-a.ts +6 -0
- package/test/fixtures/sample-project/src/circular-b.ts +6 -0
- package/test/fixtures/sample-project/src/config.ts +15 -0
- package/test/fixtures/sample-project/src/main.ts +19 -0
- package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
- package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
- package/test/fixtures/sample-project/src/types.ts +14 -0
- package/test/fixtures/sample-project/src/utils/index.ts +14 -0
- package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 7.7.1 — Integration tests: Full pipeline + CI-compatible mock [S-4]
|
|
3
|
+
*
|
|
4
|
+
* Test A — Live API: gated on GEMINI_API_KEY. Exercises real Gemini embeddings.
|
|
5
|
+
* Test B — Mock-embedder: runs in CI always. Exercises full pipeline with
|
|
6
|
+
* deterministic fake vectors — no API key required.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import * as fs from 'node:fs/promises';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import * as crypto from 'node:crypto';
|
|
13
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
14
|
+
import type { Logger } from 'winston';
|
|
15
|
+
import type { NomosConfig, ProjectMap } from '../../types/index.js';
|
|
16
|
+
import { SearchIndexer } from '../indexer.js';
|
|
17
|
+
import { VectorStore } from '../vector-store.js';
|
|
18
|
+
import { QueryEngine } from '../query-engine.js';
|
|
19
|
+
import { GraphEnricher } from '../graph-enricher.js';
|
|
20
|
+
|
|
21
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function makeLogger(): Logger {
|
|
24
|
+
return {
|
|
25
|
+
info: vi.fn(),
|
|
26
|
+
warn: vi.fn(),
|
|
27
|
+
error: vi.fn(),
|
|
28
|
+
debug: vi.fn(),
|
|
29
|
+
} as unknown as Logger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const VECTOR_DIMS = 64; // small but realistic for mock
|
|
33
|
+
|
|
34
|
+
function makeConfig(vectorStorePath: string, graphOutputDir: string): NomosConfig {
|
|
35
|
+
return {
|
|
36
|
+
execution: {
|
|
37
|
+
default_mode: 'supervised',
|
|
38
|
+
shadow_branch_prefix: 'nomos/',
|
|
39
|
+
worktree_base: '/tmp',
|
|
40
|
+
supervised_heartbeat_timeout_ms: 30_000,
|
|
41
|
+
},
|
|
42
|
+
binaries: {
|
|
43
|
+
planner: {
|
|
44
|
+
cmd: 'claude',
|
|
45
|
+
args: [],
|
|
46
|
+
pty: true,
|
|
47
|
+
total_timeout_ms: 60_000,
|
|
48
|
+
heartbeat_timeout_ms: 30_000,
|
|
49
|
+
max_output_bytes: 1_000_000,
|
|
50
|
+
usage_pattern: null,
|
|
51
|
+
},
|
|
52
|
+
reviewer: {
|
|
53
|
+
cmd: 'openai',
|
|
54
|
+
args: [],
|
|
55
|
+
pty: false,
|
|
56
|
+
total_timeout_ms: 60_000,
|
|
57
|
+
heartbeat_timeout_ms: 30_000,
|
|
58
|
+
max_output_bytes: 1_000_000,
|
|
59
|
+
usage_pattern: null,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
convergence: { score_threshold: 8, max_iterations: 3 },
|
|
63
|
+
budget: { max_tokens_per_task: 100_000, warn_at_percent: 80, cost_per_1k_tokens: {} },
|
|
64
|
+
security: {
|
|
65
|
+
sanitize_patterns: [],
|
|
66
|
+
entropy_threshold: 4.5,
|
|
67
|
+
sanitize_on: ['input', 'output'],
|
|
68
|
+
safe_commands: [],
|
|
69
|
+
redaction_label: '[REDACTED]',
|
|
70
|
+
},
|
|
71
|
+
git: { auto_commit: false, include_logs: true, commit_prefix: 'arc:', sign_commits: false },
|
|
72
|
+
review: { max_context_files: 10 },
|
|
73
|
+
graph: {
|
|
74
|
+
exclude_patterns: [],
|
|
75
|
+
ai_enrichment: false,
|
|
76
|
+
ai_model: 'gemini-pro',
|
|
77
|
+
ai_concurrency: 1,
|
|
78
|
+
ai_requests_per_minute: 60,
|
|
79
|
+
max_file_chars: 50_000,
|
|
80
|
+
core_modules_count: 5,
|
|
81
|
+
output_dir: graphOutputDir,
|
|
82
|
+
},
|
|
83
|
+
logging: { level: 'info', retain_days: 7 },
|
|
84
|
+
search: {
|
|
85
|
+
embedding_model: 'gemini-embedding-001',
|
|
86
|
+
embedding_dimensions: VECTOR_DIMS,
|
|
87
|
+
vector_store_path: vectorStorePath,
|
|
88
|
+
default_top_k: 5,
|
|
89
|
+
default_threshold: 0.0, // low threshold so mock results always pass
|
|
90
|
+
batch_size: 5,
|
|
91
|
+
embedding_requests_per_minute: 60,
|
|
92
|
+
request_timeout_ms: 30_000,
|
|
93
|
+
},
|
|
94
|
+
} as unknown as NomosConfig;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Minimal fixture: 5 files, with symbols and semantic data. */
|
|
98
|
+
function makeFixtureProjectMap(): ProjectMap {
|
|
99
|
+
return {
|
|
100
|
+
schema_version: 1,
|
|
101
|
+
generated_at: new Date().toISOString(),
|
|
102
|
+
root: '/project',
|
|
103
|
+
files: {
|
|
104
|
+
'src/errors.ts': {
|
|
105
|
+
file: 'src/errors.ts',
|
|
106
|
+
hash: 'hash-errors',
|
|
107
|
+
language: 'typescript',
|
|
108
|
+
symbols: [
|
|
109
|
+
{
|
|
110
|
+
name: 'NomosError',
|
|
111
|
+
kind: 'class',
|
|
112
|
+
line: 1,
|
|
113
|
+
end_line: 30,
|
|
114
|
+
signature: 'class NomosError extends Error',
|
|
115
|
+
exported: true,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'handleError',
|
|
119
|
+
kind: 'function',
|
|
120
|
+
line: 32,
|
|
121
|
+
end_line: 50,
|
|
122
|
+
signature: 'function handleError(err: unknown): void',
|
|
123
|
+
exported: true,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
imports: [],
|
|
127
|
+
dependents: ['src/indexer.ts', 'src/query.ts', 'src/retry.ts'],
|
|
128
|
+
dependencies: [],
|
|
129
|
+
depth: 0,
|
|
130
|
+
last_parsed_at: null,
|
|
131
|
+
semantic: {
|
|
132
|
+
overview: 'Custom error classes and error handling utilities',
|
|
133
|
+
purpose: 'Centralised error handling and retry logic for the nomos-arc system',
|
|
134
|
+
key_logic: ['extends Error with typed codes', 'provides retry boundary'],
|
|
135
|
+
usage_context: ['imported by all modules that can fail'],
|
|
136
|
+
source_hash: 'h-errors',
|
|
137
|
+
enriched_at: '2024-01-01T00:00:00.000Z',
|
|
138
|
+
model: 'gemini-pro',
|
|
139
|
+
},
|
|
140
|
+
enrichment_status: 'semantic',
|
|
141
|
+
},
|
|
142
|
+
'src/indexer.ts': {
|
|
143
|
+
file: 'src/indexer.ts',
|
|
144
|
+
hash: 'hash-indexer',
|
|
145
|
+
language: 'typescript',
|
|
146
|
+
symbols: [
|
|
147
|
+
{
|
|
148
|
+
name: 'SearchIndexer',
|
|
149
|
+
kind: 'class',
|
|
150
|
+
line: 1,
|
|
151
|
+
end_line: 100,
|
|
152
|
+
signature: 'class SearchIndexer',
|
|
153
|
+
exported: true,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
imports: [
|
|
157
|
+
{
|
|
158
|
+
source: 'src/errors.ts',
|
|
159
|
+
resolved: 'src/errors.ts',
|
|
160
|
+
symbols: ['NomosError'],
|
|
161
|
+
is_external: false,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
dependents: [],
|
|
165
|
+
dependencies: ['src/errors.ts'],
|
|
166
|
+
depth: 1,
|
|
167
|
+
last_parsed_at: null,
|
|
168
|
+
semantic: {
|
|
169
|
+
overview: 'Coordinates full and incremental vector indexing pipeline',
|
|
170
|
+
purpose: 'Builds and maintains the semantic vector search index',
|
|
171
|
+
key_logic: ['fullIndex with table-swap', 'incrementalIndex with diff'],
|
|
172
|
+
usage_context: ['invoked by arc index command'],
|
|
173
|
+
source_hash: 'h-indexer',
|
|
174
|
+
enriched_at: '2024-01-01T00:00:00.000Z',
|
|
175
|
+
model: 'gemini-pro',
|
|
176
|
+
},
|
|
177
|
+
enrichment_status: 'semantic',
|
|
178
|
+
},
|
|
179
|
+
'src/config.ts': {
|
|
180
|
+
file: 'src/config.ts',
|
|
181
|
+
hash: 'hash-config',
|
|
182
|
+
language: 'typescript',
|
|
183
|
+
symbols: [
|
|
184
|
+
{
|
|
185
|
+
name: 'loadConfig',
|
|
186
|
+
kind: 'function',
|
|
187
|
+
line: 1,
|
|
188
|
+
end_line: 40,
|
|
189
|
+
signature: 'function loadConfig(root: string): NomosConfig',
|
|
190
|
+
exported: true,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
imports: [],
|
|
194
|
+
dependents: ['src/indexer.ts', 'src/cli.ts'],
|
|
195
|
+
dependencies: [],
|
|
196
|
+
depth: 0,
|
|
197
|
+
last_parsed_at: null,
|
|
198
|
+
semantic: {
|
|
199
|
+
overview: 'Configuration loading and validation using Zod schemas',
|
|
200
|
+
purpose: 'Load and validate nomos configuration from .nomos-config.json',
|
|
201
|
+
key_logic: ['Zod validation', 'deep defaults merging'],
|
|
202
|
+
usage_context: ['called at CLI startup'],
|
|
203
|
+
source_hash: 'h-config',
|
|
204
|
+
enriched_at: '2024-01-01T00:00:00.000Z',
|
|
205
|
+
model: 'gemini-pro',
|
|
206
|
+
},
|
|
207
|
+
enrichment_status: 'semantic',
|
|
208
|
+
},
|
|
209
|
+
'src/retry.ts': {
|
|
210
|
+
file: 'src/retry.ts',
|
|
211
|
+
hash: 'hash-retry',
|
|
212
|
+
language: 'typescript',
|
|
213
|
+
symbols: [
|
|
214
|
+
{
|
|
215
|
+
name: 'withRetry',
|
|
216
|
+
kind: 'function',
|
|
217
|
+
line: 1,
|
|
218
|
+
end_line: 25,
|
|
219
|
+
signature: 'function withRetry<T>(fn: () => Promise<T>): Promise<T>',
|
|
220
|
+
exported: true,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
imports: [
|
|
224
|
+
{
|
|
225
|
+
source: 'src/errors.ts',
|
|
226
|
+
resolved: 'src/errors.ts',
|
|
227
|
+
symbols: ['NomosError'],
|
|
228
|
+
is_external: false,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
dependents: ['src/indexer.ts'],
|
|
232
|
+
dependencies: ['src/errors.ts'],
|
|
233
|
+
depth: 1,
|
|
234
|
+
last_parsed_at: null,
|
|
235
|
+
semantic: {
|
|
236
|
+
overview: 'Exponential backoff retry utility with jitter',
|
|
237
|
+
purpose: 'Retry transient failures with exponential backoff and error boundary',
|
|
238
|
+
key_logic: ['exponential backoff', 'jitter', 'max retries'],
|
|
239
|
+
usage_context: ['wraps API calls in embedder'],
|
|
240
|
+
source_hash: 'h-retry',
|
|
241
|
+
enriched_at: '2024-01-01T00:00:00.000Z',
|
|
242
|
+
model: 'gemini-pro',
|
|
243
|
+
},
|
|
244
|
+
enrichment_status: 'semantic',
|
|
245
|
+
},
|
|
246
|
+
'src/query.ts': {
|
|
247
|
+
file: 'src/query.ts',
|
|
248
|
+
hash: 'hash-query',
|
|
249
|
+
language: 'typescript',
|
|
250
|
+
symbols: [
|
|
251
|
+
{
|
|
252
|
+
name: 'QueryEngine',
|
|
253
|
+
kind: 'class',
|
|
254
|
+
line: 1,
|
|
255
|
+
end_line: 80,
|
|
256
|
+
signature: 'class QueryEngine',
|
|
257
|
+
exported: true,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
imports: [
|
|
261
|
+
{
|
|
262
|
+
source: 'src/errors.ts',
|
|
263
|
+
resolved: 'src/errors.ts',
|
|
264
|
+
symbols: ['NomosError'],
|
|
265
|
+
is_external: false,
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
dependents: [],
|
|
269
|
+
dependencies: ['src/errors.ts'],
|
|
270
|
+
depth: 1,
|
|
271
|
+
last_parsed_at: null,
|
|
272
|
+
semantic: {
|
|
273
|
+
overview: 'Semantic search query execution pipeline',
|
|
274
|
+
purpose: 'Execute semantic search queries against the vector index',
|
|
275
|
+
key_logic: ['embed query', 'vector search', 'graph enrich', 'dedup', 'rank'],
|
|
276
|
+
usage_context: ['invoked by arc search command'],
|
|
277
|
+
source_hash: 'h-query',
|
|
278
|
+
enriched_at: '2024-01-01T00:00:00.000Z',
|
|
279
|
+
model: 'gemini-pro',
|
|
280
|
+
},
|
|
281
|
+
enrichment_status: 'semantic',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
stats: {
|
|
285
|
+
total_files: 5,
|
|
286
|
+
total_symbols: 7,
|
|
287
|
+
total_edges: 5,
|
|
288
|
+
core_modules: ['src/errors.ts', 'src/config.ts'],
|
|
289
|
+
structural_only: 0,
|
|
290
|
+
semantically_enriched: 5,
|
|
291
|
+
indexed: 0,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* MockEmbedder — deterministic vectors without any API calls [S-4].
|
|
298
|
+
* Uses SHA-256 of text → repeated to fill `dims` Float32Array values.
|
|
299
|
+
* Output vectors are L2-normalised so cosine similarity (via dot product) works correctly.
|
|
300
|
+
*/
|
|
301
|
+
class MockEmbedder {
|
|
302
|
+
readonly dimensions: number;
|
|
303
|
+
|
|
304
|
+
constructor(dims: number) {
|
|
305
|
+
this.dimensions = dims;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async embedOne(text: string): Promise<Float32Array> {
|
|
309
|
+
return this._make(text);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async embedBatch(
|
|
313
|
+
texts: string[],
|
|
314
|
+
onBatchComplete?: (batchIndex: number, total: number) => void,
|
|
315
|
+
): Promise<Float32Array[]> {
|
|
316
|
+
const result = texts.map(t => this._make(t));
|
|
317
|
+
onBatchComplete?.(0, 1);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private _make(text: string): Float32Array {
|
|
322
|
+
const hash = crypto.createHash('sha256').update(text, 'utf8').digest();
|
|
323
|
+
const v = new Float32Array(this.dimensions);
|
|
324
|
+
for (let i = 0; i < this.dimensions; i++) {
|
|
325
|
+
v[i] = hash[i % hash.byteLength]! / 255.0;
|
|
326
|
+
}
|
|
327
|
+
// L2-normalise
|
|
328
|
+
let norm = 0;
|
|
329
|
+
for (let i = 0; i < v.length; i++) norm += v[i]! * v[i]!;
|
|
330
|
+
norm = Math.sqrt(norm);
|
|
331
|
+
if (norm > 0) {
|
|
332
|
+
for (let i = 0; i < v.length; i++) v[i]! / norm; // eslint-disable-line
|
|
333
|
+
for (let i = 0; i < v.length; i++) v[i] = v[i]! / norm;
|
|
334
|
+
}
|
|
335
|
+
return v;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─── Test B: Mock-embedder integration (always runs in CI) [S-4] ──────────────
|
|
340
|
+
|
|
341
|
+
describe('mock-embedder integration — full pipeline (CI-compatible)', () => {
|
|
342
|
+
let tmpDir: string;
|
|
343
|
+
let vectorStorePath: string;
|
|
344
|
+
let graphDir: string;
|
|
345
|
+
let config: NomosConfig;
|
|
346
|
+
let logger: Logger;
|
|
347
|
+
let projectRoot: string;
|
|
348
|
+
|
|
349
|
+
beforeAll(async () => {
|
|
350
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'nomos-integ-mock-'));
|
|
351
|
+
vectorStorePath = path.join(tmpDir, 'vector_index');
|
|
352
|
+
graphDir = path.join(tmpDir, 'graph');
|
|
353
|
+
projectRoot = tmpDir;
|
|
354
|
+
await fs.mkdir(graphDir, { recursive: true });
|
|
355
|
+
|
|
356
|
+
const fixture = makeFixtureProjectMap();
|
|
357
|
+
await fs.writeFile(
|
|
358
|
+
path.join(graphDir, 'project_map.json'),
|
|
359
|
+
JSON.stringify(fixture, null, 2),
|
|
360
|
+
'utf-8',
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
config = makeConfig(vectorStorePath, graphDir);
|
|
364
|
+
logger = makeLogger();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
afterAll(async () => {
|
|
368
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('full index with mock embedder completes end-to-end', async () => {
|
|
372
|
+
const mockEmbedder = new MockEmbedder(VECTOR_DIMS);
|
|
373
|
+
|
|
374
|
+
const indexer = new SearchIndexer(projectRoot, config, logger);
|
|
375
|
+
// @ts-expect-error — inject mock via private field for testing (lazy _embedder)
|
|
376
|
+
indexer['_embedder'] = mockEmbedder;
|
|
377
|
+
|
|
378
|
+
const meta = await indexer.fullIndex();
|
|
379
|
+
|
|
380
|
+
// [BLOCKER-2] status must be "complete"
|
|
381
|
+
expect(meta.status).toBe('complete');
|
|
382
|
+
|
|
383
|
+
// 5 files indexed
|
|
384
|
+
expect(meta.total_files_indexed).toBe(5);
|
|
385
|
+
|
|
386
|
+
// Symbols present → total_chunks > 5
|
|
387
|
+
expect(meta.total_chunks).toBeGreaterThan(5);
|
|
388
|
+
|
|
389
|
+
// [BLOCKER-3] model recorded in metadata
|
|
390
|
+
expect(meta.embedding_model).toBe(config.search.embedding_model);
|
|
391
|
+
expect(meta.vector_dimensions).toBe(config.search.embedding_dimensions);
|
|
392
|
+
|
|
393
|
+
// [GAP-1] No failures
|
|
394
|
+
expect(meta.failed_files.length).toBe(0);
|
|
395
|
+
|
|
396
|
+
// index-meta.json must exist
|
|
397
|
+
const metaPath = path.join(vectorStorePath, 'index-meta.json');
|
|
398
|
+
await expect(fs.access(metaPath)).resolves.toBeUndefined();
|
|
399
|
+
|
|
400
|
+
// Vector store must have records
|
|
401
|
+
const store = new VectorStore(vectorStorePath, logger, VECTOR_DIMS);
|
|
402
|
+
await store.init();
|
|
403
|
+
const count = await store.count();
|
|
404
|
+
expect(count).toBeGreaterThan(0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('search with mock embedder returns ranked results', async () => {
|
|
408
|
+
const mockEmbedder = new MockEmbedder(VECTOR_DIMS);
|
|
409
|
+
|
|
410
|
+
const engine = new QueryEngine(projectRoot, config, logger);
|
|
411
|
+
// @ts-expect-error — inject mock via lazy private field
|
|
412
|
+
engine['_embedder'] = mockEmbedder;
|
|
413
|
+
// Fix double-join: override enricher and projectMapPath with correct absolute paths
|
|
414
|
+
// @ts-expect-error
|
|
415
|
+
engine['enricher'] = new GraphEnricher(path.join(graphDir, 'project_map.json'), logger);
|
|
416
|
+
// @ts-expect-error
|
|
417
|
+
engine['projectMapPath'] = path.join(graphDir, 'project_map.json');
|
|
418
|
+
|
|
419
|
+
const results = await engine.search('error handling and retry logic');
|
|
420
|
+
|
|
421
|
+
expect(results.length).toBeGreaterThan(0);
|
|
422
|
+
|
|
423
|
+
for (const r of results) {
|
|
424
|
+
// [S-3] similarity ∈ [0, 1]
|
|
425
|
+
expect(r.similarity_score).toBeGreaterThanOrEqual(0);
|
|
426
|
+
expect(r.similarity_score).toBeLessThanOrEqual(1);
|
|
427
|
+
|
|
428
|
+
// [S-5] no vector field
|
|
429
|
+
expect(r).not.toHaveProperty('vector');
|
|
430
|
+
|
|
431
|
+
// dependency graph fields present
|
|
432
|
+
expect(typeof r.file_path).toBe('string');
|
|
433
|
+
expect(typeof r.graph_depth).toBe('number');
|
|
434
|
+
expect(typeof r.dependents_count).toBe('number');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Results sorted descending by similarity_score
|
|
438
|
+
for (let i = 1; i < results.length; i++) {
|
|
439
|
+
expect(results[i - 1]!.similarity_score).toBeGreaterThanOrEqual(
|
|
440
|
+
results[i]!.similarity_score,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('incremental index only re-embeds 1 changed file [BLOCKER-2]', async () => {
|
|
446
|
+
const mockEmbedder = new MockEmbedder(VECTOR_DIMS);
|
|
447
|
+
const embedSpy = vi.spyOn(mockEmbedder, 'embedBatch');
|
|
448
|
+
|
|
449
|
+
// Modify one file's semantic data in the fixture
|
|
450
|
+
const fixturePath = path.join(graphDir, 'project_map.json');
|
|
451
|
+
const raw = await fs.readFile(fixturePath, 'utf-8');
|
|
452
|
+
const fixture = JSON.parse(raw) as ProjectMap;
|
|
453
|
+
fixture.files['src/config.ts']!.hash = 'hash-config-MODIFIED';
|
|
454
|
+
fixture.files['src/config.ts']!.semantic!.purpose = 'Updated config loading purpose';
|
|
455
|
+
fixture.generated_at = new Date().toISOString();
|
|
456
|
+
await fs.writeFile(fixturePath, JSON.stringify(fixture, null, 2), 'utf-8');
|
|
457
|
+
|
|
458
|
+
const indexer = new SearchIndexer(projectRoot, config, logger);
|
|
459
|
+
// @ts-expect-error — inject mock via lazy private field
|
|
460
|
+
indexer['_embedder'] = mockEmbedder;
|
|
461
|
+
|
|
462
|
+
const meta = await indexer.incrementalIndex();
|
|
463
|
+
|
|
464
|
+
expect(meta.status).toBe('complete');
|
|
465
|
+
|
|
466
|
+
// Count total texts embedded across all embedBatch calls
|
|
467
|
+
const totalEmbedded = embedSpy.mock.calls.reduce(
|
|
468
|
+
(sum, call) => sum + (call[0] as string[]).length,
|
|
469
|
+
0,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
// Only chunks from 1 file should have been re-embedded
|
|
473
|
+
// src/config.ts has 1 file chunk + 1 symbol chunk = 2 chunks max
|
|
474
|
+
expect(totalEmbedded).toBeLessThanOrEqual(2);
|
|
475
|
+
expect(totalEmbedded).toBeGreaterThan(0);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('[BLOCKER-1] staging table does not exist after full index completion', async () => {
|
|
479
|
+
// Perform a fresh full index in a new temp dir to check staging cleanup
|
|
480
|
+
const freshTmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'nomos-staging-'));
|
|
481
|
+
const freshVectorPath = path.join(freshTmpDir, 'vector_index');
|
|
482
|
+
const freshGraphDir = path.join(freshTmpDir, 'graph');
|
|
483
|
+
await fs.mkdir(freshGraphDir, { recursive: true });
|
|
484
|
+
|
|
485
|
+
const fixture = makeFixtureProjectMap();
|
|
486
|
+
await fs.writeFile(
|
|
487
|
+
path.join(freshGraphDir, 'project_map.json'),
|
|
488
|
+
JSON.stringify(fixture, null, 2),
|
|
489
|
+
'utf-8',
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const freshConfig = makeConfig(freshVectorPath, freshGraphDir);
|
|
493
|
+
const freshLogger = makeLogger();
|
|
494
|
+
const freshIndexer = new SearchIndexer(freshTmpDir, freshConfig, freshLogger);
|
|
495
|
+
// @ts-expect-error — inject mock via lazy private field
|
|
496
|
+
freshIndexer['_embedder'] = new MockEmbedder(VECTOR_DIMS);
|
|
497
|
+
|
|
498
|
+
await freshIndexer.fullIndex();
|
|
499
|
+
|
|
500
|
+
// Open the store directly and verify staging table is gone
|
|
501
|
+
const store = new VectorStore(freshVectorPath, freshLogger, VECTOR_DIMS);
|
|
502
|
+
await store.init();
|
|
503
|
+
|
|
504
|
+
// promoteStagingToLive drops staging; cleanupStaging is idempotent
|
|
505
|
+
// Verify by calling cleanupStaging — it should find nothing to clean
|
|
506
|
+
const warnFn = freshLogger.warn as ReturnType<typeof vi.fn>;
|
|
507
|
+
const warnCallsBefore = warnFn.mock.calls.length;
|
|
508
|
+
await store.cleanupStaging();
|
|
509
|
+
// No new "Cleaned up orphaned staging table" warning means staging was already gone
|
|
510
|
+
const newWarnCalls = (warnFn.mock.calls as unknown[][]).slice(warnCallsBefore);
|
|
511
|
+
const stagingWarn = newWarnCalls.find(args =>
|
|
512
|
+
String(args[0]).includes('Cleaned up orphaned staging table'),
|
|
513
|
+
);
|
|
514
|
+
expect(stagingWarn).toBeUndefined();
|
|
515
|
+
|
|
516
|
+
await fs.rm(freshTmpDir, { recursive: true, force: true });
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('[TRAP-3] de-duplication rule — file-level result removed when symbol within 0.05', async () => {
|
|
520
|
+
// Use a real QueryEngine with mock embedder; the dedup logic is deterministic
|
|
521
|
+
const mockEmbedder = new MockEmbedder(VECTOR_DIMS);
|
|
522
|
+
const engine = new QueryEngine(projectRoot, config, logger);
|
|
523
|
+
// @ts-expect-error — inject mock via lazy private field
|
|
524
|
+
engine['_embedder'] = mockEmbedder;
|
|
525
|
+
// Fix double-join: override enricher and projectMapPath with correct absolute paths
|
|
526
|
+
// @ts-expect-error
|
|
527
|
+
engine['enricher'] = new GraphEnricher(path.join(graphDir, 'project_map.json'), logger);
|
|
528
|
+
// @ts-expect-error
|
|
529
|
+
engine['projectMapPath'] = path.join(graphDir, 'project_map.json');
|
|
530
|
+
|
|
531
|
+
const results = await engine.search('error handling');
|
|
532
|
+
|
|
533
|
+
// For any file that appears in both file-level and symbol-level results,
|
|
534
|
+
// if the gap is <= 0.05, the file-level result should be absent.
|
|
535
|
+
const fileResults = results.filter(r => r.type === 'file');
|
|
536
|
+
const symbolResults = results.filter(r => r.type === 'symbol');
|
|
537
|
+
|
|
538
|
+
for (const fileResult of fileResults) {
|
|
539
|
+
const symbols = symbolResults.filter(s => s.file_path === fileResult.file_path);
|
|
540
|
+
if (symbols.length > 0) {
|
|
541
|
+
const maxSymbolScore = Math.max(...symbols.map(s => s.similarity_score));
|
|
542
|
+
const gap = Math.abs(fileResult.similarity_score - maxSymbolScore);
|
|
543
|
+
// If file-level result IS present, the gap must be > 0.05
|
|
544
|
+
expect(gap).toBeGreaterThan(0.05);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// ─── Test A: Live API (requires GEMINI_API_KEY) ───────────────────────────────
|
|
551
|
+
|
|
552
|
+
describe.skipIf(!process.env['GEMINI_API_KEY'])(
|
|
553
|
+
'live API integration — full pipeline (requires GEMINI_API_KEY)',
|
|
554
|
+
() => {
|
|
555
|
+
let tmpDir: string;
|
|
556
|
+
let vectorStorePath: string;
|
|
557
|
+
let graphDir: string;
|
|
558
|
+
let config: NomosConfig;
|
|
559
|
+
let logger: Logger;
|
|
560
|
+
let projectRoot: string;
|
|
561
|
+
|
|
562
|
+
beforeAll(async () => {
|
|
563
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'nomos-integ-live-'));
|
|
564
|
+
vectorStorePath = path.join(tmpDir, 'vector_index');
|
|
565
|
+
graphDir = path.join(tmpDir, 'graph');
|
|
566
|
+
projectRoot = tmpDir;
|
|
567
|
+
await fs.mkdir(graphDir, { recursive: true });
|
|
568
|
+
|
|
569
|
+
const fixture = makeFixtureProjectMap();
|
|
570
|
+
await fs.writeFile(
|
|
571
|
+
path.join(graphDir, 'project_map.json'),
|
|
572
|
+
JSON.stringify(fixture, null, 2),
|
|
573
|
+
'utf-8',
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
config = makeConfig(vectorStorePath, graphDir);
|
|
577
|
+
logger = makeLogger();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
afterAll(async () => {
|
|
581
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('full index with real Gemini API completes', async () => {
|
|
585
|
+
const indexer = new SearchIndexer(projectRoot, config, logger);
|
|
586
|
+
const meta = await indexer.fullIndex();
|
|
587
|
+
|
|
588
|
+
// [BLOCKER-2]
|
|
589
|
+
expect(meta.status).toBe('complete');
|
|
590
|
+
expect(meta.total_files_indexed).toBe(5);
|
|
591
|
+
expect(meta.total_chunks).toBeGreaterThan(5);
|
|
592
|
+
|
|
593
|
+
// [BLOCKER-3]
|
|
594
|
+
expect(meta.embedding_model).toBe('gemini-embedding-001');
|
|
595
|
+
expect(meta.vector_dimensions).toBe(config.search.embedding_dimensions);
|
|
596
|
+
|
|
597
|
+
// [GAP-1]
|
|
598
|
+
expect(meta.failed_files.length).toBe(0);
|
|
599
|
+
|
|
600
|
+
// index-meta.json exists
|
|
601
|
+
const metaPath = path.join(vectorStorePath, 'index-meta.json');
|
|
602
|
+
await expect(fs.access(metaPath)).resolves.toBeUndefined();
|
|
603
|
+
|
|
604
|
+
// Vector store has records
|
|
605
|
+
const store = new VectorStore(vectorStorePath, logger, VECTOR_DIMS);
|
|
606
|
+
await store.init();
|
|
607
|
+
expect(await store.count()).toBeGreaterThan(0);
|
|
608
|
+
}, 120_000);
|
|
609
|
+
|
|
610
|
+
it('search returns semantically relevant results', async () => {
|
|
611
|
+
const engine = new QueryEngine(projectRoot, config, logger);
|
|
612
|
+
const results = await engine.search('error handling and retry logic');
|
|
613
|
+
|
|
614
|
+
expect(results.length).toBeGreaterThan(0);
|
|
615
|
+
|
|
616
|
+
for (const r of results) {
|
|
617
|
+
// [S-3] similarity ∈ [0, 1]
|
|
618
|
+
expect(r.similarity_score).toBeGreaterThanOrEqual(0.0);
|
|
619
|
+
expect(r.similarity_score).toBeLessThanOrEqual(1.0);
|
|
620
|
+
|
|
621
|
+
// [S-5] no vector field
|
|
622
|
+
expect(r).not.toHaveProperty('vector');
|
|
623
|
+
|
|
624
|
+
// Valid dependency fields
|
|
625
|
+
expect(typeof r.file_path).toBe('string');
|
|
626
|
+
expect(r.graph_depth).toBeGreaterThanOrEqual(0);
|
|
627
|
+
expect(typeof r.dependents_count).toBe('number');
|
|
628
|
+
|
|
629
|
+
// [TRAP-4] no stale results with depth -1
|
|
630
|
+
expect(r.graph_depth).not.toBe(-1);
|
|
631
|
+
}
|
|
632
|
+
}, 60_000);
|
|
633
|
+
|
|
634
|
+
it('incremental re-index only re-embeds changed file', async () => {
|
|
635
|
+
const fixturePath = path.join(graphDir, 'project_map.json');
|
|
636
|
+
const raw = await fs.readFile(fixturePath, 'utf-8');
|
|
637
|
+
const fixture = JSON.parse(raw) as ProjectMap;
|
|
638
|
+
fixture.files['src/retry.ts']!.hash = 'hash-retry-LIVE-MODIFIED';
|
|
639
|
+
fixture.files['src/retry.ts']!.semantic!.purpose = 'Updated retry purpose for live test';
|
|
640
|
+
fixture.generated_at = new Date().toISOString();
|
|
641
|
+
await fs.writeFile(fixturePath, JSON.stringify(fixture, null, 2), 'utf-8');
|
|
642
|
+
|
|
643
|
+
const indexer = new SearchIndexer(projectRoot, config, logger);
|
|
644
|
+
const meta = await indexer.incrementalIndex();
|
|
645
|
+
|
|
646
|
+
expect(meta.status).toBe('complete');
|
|
647
|
+
}, 60_000);
|
|
648
|
+
},
|
|
649
|
+
);
|