@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,212 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { NomosError } from './errors.js';
|
|
6
|
+
import type { NomosConfig } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
// ── Per-binary defaults ──────────────────────────────────────────────────────
|
|
9
|
+
// IMPORTANT: planner and reviewer have DIFFERENT defaults.
|
|
10
|
+
// Using z.object().default({}) at each nested level performs shallow merge ONLY at
|
|
11
|
+
// the top sub-object level. To correctly deep-merge a partial user config (e.g., only
|
|
12
|
+
// providing `cmd`), every individual field must carry its own .default() value.
|
|
13
|
+
// This ensures `{ cmd: 'my-claude' }` correctly inherits pty: true, timeouts, etc.
|
|
14
|
+
//
|
|
15
|
+
// Zod v4 note: .default() on z.object() requires the OUTPUT type as the default value,
|
|
16
|
+
// not the input type. We use factory functions `() => SubSchema.parse({})` so TypeScript
|
|
17
|
+
// is satisfied while preserving identical runtime behavior to the spec's `.default({})`.
|
|
18
|
+
|
|
19
|
+
const PlannerBinarySchema = z.object({
|
|
20
|
+
cmd: z.string().default('claude'),
|
|
21
|
+
args: z.array(z.string()).default([]),
|
|
22
|
+
pty: z.boolean().default(true),
|
|
23
|
+
total_timeout_ms: z.number().positive().default(300000),
|
|
24
|
+
heartbeat_timeout_ms: z.number().positive().default(120000),
|
|
25
|
+
max_output_bytes: z.number().positive().default(1048576),
|
|
26
|
+
usage_pattern: z.string().nullable().default('Tokens used:\\s*(\\d+)'),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const ReviewerBinarySchema = z.object({
|
|
30
|
+
cmd: z.string().default('codex'),
|
|
31
|
+
args: z.array(z.string()).default(['-q', '--full-auto']),
|
|
32
|
+
pty: z.boolean().default(false),
|
|
33
|
+
total_timeout_ms: z.number().positive().default(120000),
|
|
34
|
+
heartbeat_timeout_ms: z.number().positive().default(120000),
|
|
35
|
+
max_output_bytes: z.number().positive().default(524288),
|
|
36
|
+
usage_pattern: z.string().nullable().default(null),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const ExecutionSchema = z.object({
|
|
40
|
+
default_mode: z.enum(['supervised', 'dry-run']).default('supervised'),
|
|
41
|
+
shadow_branch_prefix: z.string().default('nomos/'),
|
|
42
|
+
worktree_base: z.string().default('/tmp/nomos-worktrees/'),
|
|
43
|
+
supervised_heartbeat_timeout_ms: z.number().positive().default(300000),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const BinariesSchema = z.object({
|
|
47
|
+
planner: PlannerBinarySchema.default(() => PlannerBinarySchema.parse({})),
|
|
48
|
+
reviewer: ReviewerBinarySchema.default(() => ReviewerBinarySchema.parse({})),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const ConvergenceSchema = z.object({
|
|
52
|
+
score_threshold: z.number().min(0).max(1).default(0.9),
|
|
53
|
+
max_iterations: z.number().int().positive().default(3),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const BudgetSchema = z.object({
|
|
57
|
+
max_tokens_per_task: z.number().positive().default(100000),
|
|
58
|
+
warn_at_percent: z.number().min(0).max(100).default(80),
|
|
59
|
+
// RT2-4.3 fix: cost_per_1k_tokens supports both flat number (backward compat,
|
|
60
|
+
// treated as output rate with input = rate/5) and { input, output } objects.
|
|
61
|
+
cost_per_1k_tokens: z.record(z.string(), z.union([
|
|
62
|
+
z.number(),
|
|
63
|
+
z.object({ input: z.number(), output: z.number() }),
|
|
64
|
+
])).default({
|
|
65
|
+
claude: { input: 0.003, output: 0.015 },
|
|
66
|
+
codex: { input: 0.0005, output: 0.002 },
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const SecuritySchema = z.object({
|
|
71
|
+
sanitize_patterns: z.array(z.string()).default([]),
|
|
72
|
+
entropy_threshold: z.number().positive().default(4.5),
|
|
73
|
+
sanitize_on: z.array(z.enum(['input', 'output'])).default(['input']),
|
|
74
|
+
safe_commands: z.array(z.string()).default([]),
|
|
75
|
+
redaction_label: z.string().default('[REDACTED]'),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const GitSchema = z.object({
|
|
79
|
+
auto_commit: z.boolean().default(true),
|
|
80
|
+
include_logs: z.boolean().default(false),
|
|
81
|
+
commit_prefix: z.string().default('[nomos]'),
|
|
82
|
+
sign_commits: z.boolean().default(false),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const ReviewSchema = z.object({
|
|
86
|
+
max_context_files: z.number().int().positive().default(5),
|
|
87
|
+
// Budget note: each affected file adds ~50 lines to the reviewer prompt.
|
|
88
|
+
// At default 5 files this is roughly 250 extra lines (~1500 tokens).
|
|
89
|
+
// Tune downward if reviewer token costs are a concern.
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const LoggingSchema = z.object({
|
|
93
|
+
level: z.string().default('info'),
|
|
94
|
+
retain_days: z.number().positive().default(30),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const SearchConfigSchema = z.object({
|
|
98
|
+
embedding_model: z.string().default('gemini-embedding-001'),
|
|
99
|
+
embedding_dimensions: z.number().int().positive().default(768),
|
|
100
|
+
vector_store_path: z.string().default('tasks-management/graph/vector_index'),
|
|
101
|
+
default_top_k: z.number().int().positive().default(5),
|
|
102
|
+
default_threshold: z.number().min(0).max(1).default(0.7),
|
|
103
|
+
batch_size: z.number().int().positive().max(100).default(50),
|
|
104
|
+
embedding_requests_per_minute: z.number().int().positive().default(300),
|
|
105
|
+
request_timeout_ms: z.number().int().positive().default(30_000),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const AuthSchema = z.object({
|
|
109
|
+
client_id: z.string().optional(),
|
|
110
|
+
// SECURITY: client_secret intentionally excluded from project config.
|
|
111
|
+
// Project config (.nomos-config.json) is committed to git — secrets must not live here.
|
|
112
|
+
// Resolution order: NOMOS_GOOGLE_CLIENT_SECRET env var → interactive prompt → ~/.nomos/credentials.json (saved after first login).
|
|
113
|
+
credentials_path: z.string().default(
|
|
114
|
+
path.join(os.homedir(), '.nomos', 'credentials.json'),
|
|
115
|
+
),
|
|
116
|
+
redirect_port: z.number().int().positive().default(3000),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const GraphConfigSchema = z.object({
|
|
120
|
+
exclude_patterns: z.array(z.string()).default(['node_modules', 'dist', '*.test.*', '*.spec.*', '*.semantic.md']),
|
|
121
|
+
ai_enrichment: z.boolean().default(true),
|
|
122
|
+
ai_model: z.string().default('gemini-1.5-flash'),
|
|
123
|
+
ai_concurrency: z.number().int().positive().default(5),
|
|
124
|
+
ai_requests_per_minute: z.number().int().positive().default(14),
|
|
125
|
+
max_file_chars: z.number().positive().default(4000),
|
|
126
|
+
core_modules_count: z.number().int().positive().default(10),
|
|
127
|
+
output_dir: z.string().default('tasks-management/graph'),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const NomosConfigSchema = z.object({
|
|
131
|
+
execution: ExecutionSchema.default(() => ExecutionSchema.parse({})),
|
|
132
|
+
binaries: BinariesSchema.default(() => BinariesSchema.parse({})),
|
|
133
|
+
convergence: ConvergenceSchema.default(() => ConvergenceSchema.parse({})),
|
|
134
|
+
budget: BudgetSchema.default(() => BudgetSchema.parse({})),
|
|
135
|
+
security: SecuritySchema.default(() => SecuritySchema.parse({})),
|
|
136
|
+
git: GitSchema.default(() => GitSchema.parse({})),
|
|
137
|
+
review: ReviewSchema.default(() => ReviewSchema.parse({})),
|
|
138
|
+
graph: GraphConfigSchema.default(() => GraphConfigSchema.parse({})),
|
|
139
|
+
logging: LoggingSchema.default(() => LoggingSchema.parse({})),
|
|
140
|
+
search: SearchConfigSchema.default(() => SearchConfigSchema.parse({})),
|
|
141
|
+
auth: AuthSchema.default(() => AuthSchema.parse({})),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns a fully-defaulted NomosConfig by parsing an empty object through the Zod schema.
|
|
146
|
+
* Every field receives its default value. Used by `arc init` (no config file exists yet)
|
|
147
|
+
* and in tests that need a baseline config without a file on disk.
|
|
148
|
+
*
|
|
149
|
+
* RTV-3 fix: This function was referenced in factory.ts but never defined.
|
|
150
|
+
*/
|
|
151
|
+
export function getDefaultConfig(): NomosConfig {
|
|
152
|
+
return NomosConfigSchema.parse({}) as NomosConfig;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Walk from startDir up to filesystem root looking for `.nomos-config.json`.
|
|
157
|
+
* Returns the absolute path to the config file if found.
|
|
158
|
+
* Throws NomosError('config_not_found') if not found at any level.
|
|
159
|
+
*/
|
|
160
|
+
export function findConfigFile(startDir: string): string {
|
|
161
|
+
const resolvedStart = fs.realpathSync(startDir);
|
|
162
|
+
let current = resolvedStart;
|
|
163
|
+
|
|
164
|
+
while (true) {
|
|
165
|
+
const candidate = path.join(current, '.nomos-config.json');
|
|
166
|
+
if (fs.existsSync(candidate)) {
|
|
167
|
+
return candidate;
|
|
168
|
+
}
|
|
169
|
+
const parent = path.dirname(current);
|
|
170
|
+
if (parent === current) {
|
|
171
|
+
// Reached filesystem root — not found
|
|
172
|
+
throw new NomosError(
|
|
173
|
+
'config_not_found',
|
|
174
|
+
'No .nomos-config.json found. Run: arc init to scaffold a new project.',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
current = parent;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Load and validate the nomos-arc config, starting the walk-up from startDir.
|
|
183
|
+
* Returns the parsed config and the project root directory (directory containing
|
|
184
|
+
* the config file).
|
|
185
|
+
*/
|
|
186
|
+
export function loadConfig(startDir?: string): { config: NomosConfig; projectRoot: string } {
|
|
187
|
+
const configPath = findConfigFile(startDir ?? process.cwd());
|
|
188
|
+
const projectRoot = path.dirname(configPath);
|
|
189
|
+
|
|
190
|
+
let raw: unknown;
|
|
191
|
+
try {
|
|
192
|
+
raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
193
|
+
} catch (err) {
|
|
194
|
+
throw new NomosError(
|
|
195
|
+
'config_invalid',
|
|
196
|
+
`Failed to read .nomos-config.json: ${String(err)}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result = NomosConfigSchema.safeParse(raw);
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
const fieldPaths = result.error.issues
|
|
203
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
204
|
+
.join('; ');
|
|
205
|
+
throw new NomosError(
|
|
206
|
+
'config_invalid',
|
|
207
|
+
`Invalid .nomos-config.json — ${fieldPaths}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { config: result.data as NomosConfig, projectRoot };
|
|
212
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type NomosErrorCode =
|
|
2
|
+
| 'config_not_found'
|
|
3
|
+
| 'config_invalid'
|
|
4
|
+
| 'binary_not_found'
|
|
5
|
+
| 'task_not_found'
|
|
6
|
+
| 'task_exists'
|
|
7
|
+
| 'invalid_transition'
|
|
8
|
+
| 'state_locked'
|
|
9
|
+
| 'convergence_failed'
|
|
10
|
+
| 'review_failed'
|
|
11
|
+
| 'budget_exceeded'
|
|
12
|
+
| 'no_tty'
|
|
13
|
+
| 'worktree_creation_failed'
|
|
14
|
+
| 'worktree_missing'
|
|
15
|
+
| 'worktree_unrecoverable'
|
|
16
|
+
| 'branch_exists'
|
|
17
|
+
| 'wrong_branch'
|
|
18
|
+
| 'dirty_working_tree'
|
|
19
|
+
| 'path_traversal'
|
|
20
|
+
| 'state_migration_failed'
|
|
21
|
+
| 'invalid_frontmatter'
|
|
22
|
+
| 'rules_missing'
|
|
23
|
+
| 'no_changes'
|
|
24
|
+
| 'secrets_detected' // RT2-5.1 fix: context_files contain secrets
|
|
25
|
+
| 'base_commit_unreachable' // RT2-2.1 fix: baseCommit SHA orphaned after rebase
|
|
26
|
+
| 'certificate_not_eligible' // task status not approved/merged
|
|
27
|
+
| 'certificate_not_found' // certificate file not found for --verify
|
|
28
|
+
| 'certificate_invalid' // certificate failed validation or parsing
|
|
29
|
+
| 'graph_map_not_found'
|
|
30
|
+
| 'graph_parse_error'
|
|
31
|
+
| 'graph_ai_key_missing'
|
|
32
|
+
| 'graph_write_failed'
|
|
33
|
+
| 'drift_baseline_not_found'
|
|
34
|
+
| 'drift_parse_error'
|
|
35
|
+
| 'search_index_not_found'
|
|
36
|
+
| 'search_index_failed'
|
|
37
|
+
| 'search_index_corrupted'
|
|
38
|
+
| 'search_embedding_failed'
|
|
39
|
+
| 'search_query_failed'
|
|
40
|
+
| 'search_api_key_missing'
|
|
41
|
+
| 'auth_not_logged_in'
|
|
42
|
+
| 'auth_token_expired'
|
|
43
|
+
| 'auth_login_failed'
|
|
44
|
+
| 'auth_client_config_missing';
|
|
45
|
+
|
|
46
|
+
export class NomosError extends Error {
|
|
47
|
+
constructor(
|
|
48
|
+
public readonly code: NomosErrorCode,
|
|
49
|
+
message: string,
|
|
50
|
+
) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'NomosError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { createLogger } from './logger.js';
|
|
3
|
+
import { loadConfig, NomosConfigSchema } from './config.js';
|
|
4
|
+
import { StateManager } from './state.js';
|
|
5
|
+
import { GitAdapter } from '../adapters/git.js';
|
|
6
|
+
import { PtyAdapter } from '../adapters/pty.js';
|
|
7
|
+
import { StdioAdapter } from '../adapters/stdio.js';
|
|
8
|
+
import { Orchestrator } from './orchestrator.js';
|
|
9
|
+
import type { NomosConfig } from '../types/index.js';
|
|
10
|
+
|
|
11
|
+
export async function createOrchestrator(options?: {
|
|
12
|
+
skipConfig?: boolean;
|
|
13
|
+
projectRoot?: string; // E5 fix: explicit root avoids process.chdir() in tests
|
|
14
|
+
}): Promise<{ orchestrator: Orchestrator; config: NomosConfig; projectRoot: string }> {
|
|
15
|
+
|
|
16
|
+
if (options?.skipConfig) {
|
|
17
|
+
// arc init (project scaffold) — no config file exists yet.
|
|
18
|
+
// RTV-3 fix: use NomosConfigSchema.parse({}) instead of the previously-undefined getDefaultConfig()
|
|
19
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
20
|
+
const config = NomosConfigSchema.parse({}) as NomosConfig;
|
|
21
|
+
const logger = createLogger('info');
|
|
22
|
+
const stateDir = path.join(projectRoot, 'tasks-management', 'state');
|
|
23
|
+
const stateManager = new StateManager(stateDir, logger);
|
|
24
|
+
const gitAdapter = new GitAdapter(projectRoot, config, logger);
|
|
25
|
+
const ptyAdapter = new PtyAdapter(logger);
|
|
26
|
+
const stdioAdapter = new StdioAdapter(logger);
|
|
27
|
+
return {
|
|
28
|
+
orchestrator: new Orchestrator(config, projectRoot, stateManager, gitAdapter, ptyAdapter, stdioAdapter, logger),
|
|
29
|
+
config,
|
|
30
|
+
projectRoot,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { config, projectRoot } = loadConfig(options?.projectRoot);
|
|
35
|
+
const logger = createLogger(config.logging.level, path.join(projectRoot, 'tasks-management', 'logs'));
|
|
36
|
+
logger.debug(`Project root: ${projectRoot}`);
|
|
37
|
+
const stateDir = path.join(projectRoot, 'tasks-management', 'state');
|
|
38
|
+
const stateManager = new StateManager(stateDir, logger);
|
|
39
|
+
// M-6 fix: clean orphaned .tmp files from crashed writes on startup
|
|
40
|
+
stateManager.cleanupTempFiles(stateDir);
|
|
41
|
+
const gitAdapter = new GitAdapter(projectRoot, config, logger);
|
|
42
|
+
const ptyAdapter = new PtyAdapter(logger);
|
|
43
|
+
const stdioAdapter = new StdioAdapter(logger);
|
|
44
|
+
return {
|
|
45
|
+
orchestrator: new Orchestrator(config, projectRoot, stateManager, gitAdapter, ptyAdapter, stdioAdapter, logger),
|
|
46
|
+
config,
|
|
47
|
+
projectRoot,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { GraphBuilder } from '../builder.js';
|
|
3
|
+
import type { FileNode, NomosConfig } from '../../../types/index.js';
|
|
4
|
+
|
|
5
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function makeConfig(overrides: Partial<NomosConfig['graph']> = {}): NomosConfig['graph'] {
|
|
8
|
+
return {
|
|
9
|
+
exclude_patterns: [],
|
|
10
|
+
ai_enrichment: false,
|
|
11
|
+
ai_model: 'gemini-1.5-flash',
|
|
12
|
+
ai_concurrency: 5,
|
|
13
|
+
ai_requests_per_minute: 14,
|
|
14
|
+
max_file_chars: 4000,
|
|
15
|
+
core_modules_count: 3,
|
|
16
|
+
output_dir: 'tasks-management/graph',
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeLogger() {
|
|
22
|
+
return { warn: vi.fn(), info: vi.fn() };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a minimal FileNode with only the fields needed for GraphBuilder tests.
|
|
27
|
+
* Dependencies / dependents / depth are intentionally left at defaults so BLK-3
|
|
28
|
+
* reset behavior can be verified.
|
|
29
|
+
*/
|
|
30
|
+
function makeNode(
|
|
31
|
+
file: string,
|
|
32
|
+
imports: Array<{ resolved: string | null; is_external: boolean }> = [],
|
|
33
|
+
// Pre-populate with stale data to verify BLK-3 reset
|
|
34
|
+
staleData: Partial<Pick<FileNode, 'dependencies' | 'dependents' | 'depth'>> = {},
|
|
35
|
+
): FileNode {
|
|
36
|
+
return {
|
|
37
|
+
file,
|
|
38
|
+
hash: 'sha256:abc',
|
|
39
|
+
language: 'typescript',
|
|
40
|
+
symbols: [],
|
|
41
|
+
imports: imports.map((imp) => ({
|
|
42
|
+
source: imp.resolved ?? 'unknown',
|
|
43
|
+
resolved: imp.resolved,
|
|
44
|
+
symbols: [],
|
|
45
|
+
is_external: imp.is_external,
|
|
46
|
+
})),
|
|
47
|
+
dependents: staleData.dependents ?? [],
|
|
48
|
+
dependencies: staleData.dependencies ?? [],
|
|
49
|
+
depth: staleData.depth ?? 0,
|
|
50
|
+
last_parsed_at: null,
|
|
51
|
+
semantic: null,
|
|
52
|
+
enrichment_status: 'structural',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
describe('GraphBuilder', () => {
|
|
59
|
+
let logger: ReturnType<typeof makeLogger>;
|
|
60
|
+
let builder: GraphBuilder;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
logger = makeLogger();
|
|
64
|
+
builder = new GraphBuilder(makeConfig(), logger);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── Test 1: Linear chain A → B → C ───────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
it('linear chain: A→B→C → depths A=0, B=1, C=2', () => {
|
|
70
|
+
const nodes = new Map<string, FileNode>([
|
|
71
|
+
['A.ts', makeNode('A.ts', [{ resolved: 'B.ts', is_external: false }])],
|
|
72
|
+
['B.ts', makeNode('B.ts', [{ resolved: 'C.ts', is_external: false }])],
|
|
73
|
+
['C.ts', makeNode('C.ts', [])],
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
builder.build(nodes);
|
|
77
|
+
|
|
78
|
+
expect(nodes.get('A.ts')!.depth).toBe(0);
|
|
79
|
+
expect(nodes.get('B.ts')!.depth).toBe(1);
|
|
80
|
+
expect(nodes.get('C.ts')!.depth).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── Test 2: Fan-in A→C, B→C ──────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
it('fan-in: A→C, B→C → C highest depth, A and B = 0', () => {
|
|
86
|
+
const nodes = new Map<string, FileNode>([
|
|
87
|
+
['A.ts', makeNode('A.ts', [{ resolved: 'C.ts', is_external: false }])],
|
|
88
|
+
['B.ts', makeNode('B.ts', [{ resolved: 'C.ts', is_external: false }])],
|
|
89
|
+
['C.ts', makeNode('C.ts', [])],
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
builder.build(nodes);
|
|
93
|
+
|
|
94
|
+
expect(nodes.get('A.ts')!.depth).toBe(0);
|
|
95
|
+
expect(nodes.get('B.ts')!.depth).toBe(0);
|
|
96
|
+
expect(nodes.get('C.ts')!.depth).toBeGreaterThan(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── Test 3: Forward/reverse edges correctly populated ────────────────────
|
|
100
|
+
|
|
101
|
+
it('populates dependencies and dependents correctly', () => {
|
|
102
|
+
const nodes = new Map<string, FileNode>([
|
|
103
|
+
['A.ts', makeNode('A.ts', [{ resolved: 'B.ts', is_external: false }])],
|
|
104
|
+
['B.ts', makeNode('B.ts', [])],
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
builder.build(nodes);
|
|
108
|
+
|
|
109
|
+
expect(nodes.get('A.ts')!.dependencies).toEqual(['B.ts']);
|
|
110
|
+
expect(nodes.get('A.ts')!.dependents).toEqual([]);
|
|
111
|
+
expect(nodes.get('B.ts')!.dependencies).toEqual([]);
|
|
112
|
+
expect(nodes.get('B.ts')!.dependents).toEqual(['A.ts']);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── Test 4: 2-node cycle A ↔ B ───────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
it('2-node cycle: A↔B → both get depth > 0, warning logged', () => {
|
|
118
|
+
const nodes = new Map<string, FileNode>([
|
|
119
|
+
['A.ts', makeNode('A.ts', [{ resolved: 'B.ts', is_external: false }])],
|
|
120
|
+
['B.ts', makeNode('B.ts', [{ resolved: 'A.ts', is_external: false }])],
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
builder.build(nodes);
|
|
124
|
+
|
|
125
|
+
const dA = nodes.get('A.ts')!.depth;
|
|
126
|
+
const dB = nodes.get('B.ts')!.depth;
|
|
127
|
+
// Both assigned the same cycle depth (both start from external edge depth -1 → 0)
|
|
128
|
+
expect(dA).toBe(dB);
|
|
129
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
130
|
+
expect.stringContaining('Circular dependency detected'),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Test 5: 3-node cycle A → B → C → A ───────────────────────────────────
|
|
135
|
+
|
|
136
|
+
it('3-node cycle: A→B→C→A → all three get cycle depth, warning with full path', () => {
|
|
137
|
+
const nodes = new Map<string, FileNode>([
|
|
138
|
+
['A.ts', makeNode('A.ts', [{ resolved: 'B.ts', is_external: false }])],
|
|
139
|
+
['B.ts', makeNode('B.ts', [{ resolved: 'C.ts', is_external: false }])],
|
|
140
|
+
['C.ts', makeNode('C.ts', [{ resolved: 'A.ts', is_external: false }])],
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
builder.build(nodes);
|
|
144
|
+
|
|
145
|
+
// All three must have a depth assigned
|
|
146
|
+
const depths = [
|
|
147
|
+
nodes.get('A.ts')!.depth,
|
|
148
|
+
nodes.get('B.ts')!.depth,
|
|
149
|
+
nodes.get('C.ts')!.depth,
|
|
150
|
+
];
|
|
151
|
+
expect(depths.every((d) => d >= 0)).toBe(true);
|
|
152
|
+
|
|
153
|
+
// Warning should mention a cycle
|
|
154
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
155
|
+
expect.stringContaining('Circular dependency detected'),
|
|
156
|
+
);
|
|
157
|
+
// The logged path should include all three nodes
|
|
158
|
+
const warnCall = logger.warn.mock.calls.find((c) =>
|
|
159
|
+
c[0].includes('Circular dependency detected'),
|
|
160
|
+
);
|
|
161
|
+
expect(warnCall![0]).toMatch(/A\.ts/);
|
|
162
|
+
expect(warnCall![0]).toMatch(/B\.ts/);
|
|
163
|
+
expect(warnCall![0]).toMatch(/C\.ts/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── Test 6: [BLK-3] Incremental — second run clears stale edges ──────────
|
|
167
|
+
|
|
168
|
+
it('[BLK-3] second build() on same nodes does NOT accumulate stale edges', () => {
|
|
169
|
+
const nodes = new Map<string, FileNode>([
|
|
170
|
+
['A.ts', makeNode('A.ts', [{ resolved: 'B.ts', is_external: false }])],
|
|
171
|
+
['B.ts', makeNode('B.ts', [])],
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
// First build
|
|
175
|
+
builder.build(nodes);
|
|
176
|
+
expect(nodes.get('A.ts')!.dependencies).toEqual(['B.ts']);
|
|
177
|
+
expect(nodes.get('B.ts')!.dependents).toEqual(['A.ts']);
|
|
178
|
+
|
|
179
|
+
// Mutate imports (A no longer imports B)
|
|
180
|
+
nodes.get('A.ts')!.imports = [];
|
|
181
|
+
|
|
182
|
+
// Second build — stale edges must be gone
|
|
183
|
+
builder.build(nodes);
|
|
184
|
+
expect(nodes.get('A.ts')!.dependencies).toEqual([]);
|
|
185
|
+
expect(nodes.get('B.ts')!.dependents).toEqual([]);
|
|
186
|
+
expect(nodes.get('A.ts')!.depth).toBe(0);
|
|
187
|
+
expect(nodes.get('B.ts')!.depth).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── Test 7: [GAP-6] Disconnected cycles ──────────────────────────────────
|
|
191
|
+
|
|
192
|
+
it('[GAP-6] disconnected cycles A↔B and C↔D produce distinct warnings', () => {
|
|
193
|
+
const nodes = new Map<string, FileNode>([
|
|
194
|
+
['A.ts', makeNode('A.ts', [{ resolved: 'B.ts', is_external: false }])],
|
|
195
|
+
['B.ts', makeNode('B.ts', [{ resolved: 'A.ts', is_external: false }])],
|
|
196
|
+
['C.ts', makeNode('C.ts', [{ resolved: 'D.ts', is_external: false }])],
|
|
197
|
+
['D.ts', makeNode('D.ts', [{ resolved: 'C.ts', is_external: false }])],
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
builder.build(nodes);
|
|
201
|
+
|
|
202
|
+
const warnCalls = logger.warn.mock.calls.filter((c) =>
|
|
203
|
+
c[0].includes('Circular dependency detected'),
|
|
204
|
+
);
|
|
205
|
+
expect(warnCalls.length).toBeGreaterThanOrEqual(2);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ── Test 8: core_modules extraction ──────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
it('core_modules returns top N files by depth descending', () => {
|
|
211
|
+
const nodes = new Map<string, FileNode>([
|
|
212
|
+
['entry.ts', makeNode('entry.ts', [{ resolved: 'mid.ts', is_external: false }])],
|
|
213
|
+
['mid.ts', makeNode('mid.ts', [{ resolved: 'core.ts', is_external: false }])],
|
|
214
|
+
['core.ts', makeNode('core.ts', [])],
|
|
215
|
+
['util.ts', makeNode('util.ts', [])],
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
const stats = builder.build(nodes);
|
|
219
|
+
|
|
220
|
+
// core.ts has depth 2 (deepest), should appear first in core_modules
|
|
221
|
+
expect(stats.core_modules[0]).toBe('core.ts');
|
|
222
|
+
expect(stats.core_modules.length).toBeLessThanOrEqual(3); // config.core_modules_count = 3
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ── Test 9: Stats accuracy ────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
it('stats: total_files, total_symbols, total_edges are accurate', () => {
|
|
228
|
+
const nodeA: FileNode = {
|
|
229
|
+
...makeNode('A.ts', [{ resolved: 'B.ts', is_external: false }]),
|
|
230
|
+
symbols: [
|
|
231
|
+
{ name: 'foo', kind: 'function', line: 1, end_line: 5, signature: null, exported: true },
|
|
232
|
+
{ name: 'Bar', kind: 'class', line: 7, end_line: 15, signature: null, exported: false },
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
const nodeB: FileNode = {
|
|
236
|
+
...makeNode('B.ts', []),
|
|
237
|
+
symbols: [
|
|
238
|
+
{ name: 'baz', kind: 'variable', line: 1, end_line: 1, signature: null, exported: true },
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const nodes = new Map<string, FileNode>([
|
|
243
|
+
['A.ts', nodeA],
|
|
244
|
+
['B.ts', nodeB],
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
const stats = builder.build(nodes);
|
|
248
|
+
|
|
249
|
+
expect(stats.total_files).toBe(2);
|
|
250
|
+
expect(stats.total_symbols).toBe(3); // 2 from A + 1 from B
|
|
251
|
+
expect(stats.total_edges).toBe(1); // A → B
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── Bonus: external/null imports are not counted as edges ─────────────────
|
|
255
|
+
|
|
256
|
+
it('does not count external or unresolved imports as edges', () => {
|
|
257
|
+
const nodes = new Map<string, FileNode>([
|
|
258
|
+
[
|
|
259
|
+
'A.ts',
|
|
260
|
+
makeNode('A.ts', [
|
|
261
|
+
{ resolved: null, is_external: true }, // external package
|
|
262
|
+
{ resolved: null, is_external: false }, // unresolvable relative
|
|
263
|
+
{ resolved: 'B.ts', is_external: false }, // real internal edge
|
|
264
|
+
]),
|
|
265
|
+
],
|
|
266
|
+
['B.ts', makeNode('B.ts', [])],
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
const stats = builder.build(nodes);
|
|
270
|
+
expect(stats.total_edges).toBe(1);
|
|
271
|
+
});
|
|
272
|
+
});
|