@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,191 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { loadConfig, getDefaultConfig, findConfigFile } from '../config.js';
|
|
6
|
+
import { NomosError } from '../errors.js';
|
|
7
|
+
|
|
8
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTmpDir(): string {
|
|
11
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'nomos-config-test-'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeConfig(dir: string, content: unknown): string {
|
|
15
|
+
const cfgPath = path.join(dir, '.nomos-config.json');
|
|
16
|
+
fs.writeFileSync(cfgPath, JSON.stringify(content), 'utf-8');
|
|
17
|
+
return cfgPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe('findConfigFile — walk-up discovery', () => {
|
|
23
|
+
let root: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
root = makeTmpDir();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('finds config in the start directory', () => {
|
|
34
|
+
writeConfig(root, {});
|
|
35
|
+
const found = findConfigFile(root);
|
|
36
|
+
expect(found).toBe(path.join(root, '.nomos-config.json'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('finds config in a parent directory when starting from a nested child', () => {
|
|
40
|
+
writeConfig(root, {});
|
|
41
|
+
const nested = path.join(root, 'a', 'b', 'c');
|
|
42
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
43
|
+
const found = findConfigFile(nested);
|
|
44
|
+
expect(found).toBe(path.join(root, '.nomos-config.json'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws config_not_found when no config exists in any ancestor', () => {
|
|
48
|
+
// Use a fresh temp dir with no config file at all
|
|
49
|
+
const empty = makeTmpDir();
|
|
50
|
+
try {
|
|
51
|
+
expect(() => findConfigFile(empty)).toThrowError(
|
|
52
|
+
expect.objectContaining({ code: 'config_not_found' }),
|
|
53
|
+
);
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(empty, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('loadConfig — defaults and deep-merge', () => {
|
|
61
|
+
let root: string;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
root = makeTmpDir();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('loads empty config and applies all defaults', () => {
|
|
72
|
+
writeConfig(root, {});
|
|
73
|
+
const { config, projectRoot } = loadConfig(root);
|
|
74
|
+
|
|
75
|
+
expect(projectRoot).toBe(root);
|
|
76
|
+
expect(config.execution.default_mode).toBe('supervised');
|
|
77
|
+
expect(config.convergence.score_threshold).toBe(0.9);
|
|
78
|
+
expect(config.convergence.max_iterations).toBe(3);
|
|
79
|
+
expect(config.binaries.planner.cmd).toBe('claude');
|
|
80
|
+
expect(config.binaries.planner.pty).toBe(true);
|
|
81
|
+
expect(config.binaries.reviewer.cmd).toBe('codex');
|
|
82
|
+
expect(config.binaries.reviewer.pty).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('applies correct planner defaults', () => {
|
|
86
|
+
writeConfig(root, {});
|
|
87
|
+
const { config } = loadConfig(root);
|
|
88
|
+
expect(config.binaries.planner.heartbeat_timeout_ms).toBe(120000);
|
|
89
|
+
expect(config.binaries.planner.total_timeout_ms).toBe(300000);
|
|
90
|
+
expect(config.binaries.planner.max_output_bytes).toBe(1048576);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('applies correct reviewer defaults', () => {
|
|
94
|
+
writeConfig(root, {});
|
|
95
|
+
const { config } = loadConfig(root);
|
|
96
|
+
expect(config.binaries.reviewer.heartbeat_timeout_ms).toBe(120000);
|
|
97
|
+
expect(config.binaries.reviewer.total_timeout_ms).toBe(120000);
|
|
98
|
+
expect(config.binaries.reviewer.max_output_bytes).toBe(524288);
|
|
99
|
+
expect(config.binaries.reviewer.usage_pattern).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('minimal config — only cmd provided — does not wipe other planner defaults (deep-merge)', () => {
|
|
103
|
+
writeConfig(root, {
|
|
104
|
+
binaries: {
|
|
105
|
+
planner: { cmd: 'my-claude' },
|
|
106
|
+
reviewer: { cmd: 'my-codex' },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const { config } = loadConfig(root);
|
|
110
|
+
|
|
111
|
+
// Overridden
|
|
112
|
+
expect(config.binaries.planner.cmd).toBe('my-claude');
|
|
113
|
+
expect(config.binaries.reviewer.cmd).toBe('my-codex');
|
|
114
|
+
|
|
115
|
+
// Planner defaults must survive
|
|
116
|
+
expect(config.binaries.planner.pty).toBe(true);
|
|
117
|
+
expect(config.binaries.planner.heartbeat_timeout_ms).toBe(120000);
|
|
118
|
+
expect(config.binaries.planner.total_timeout_ms).toBe(300000);
|
|
119
|
+
|
|
120
|
+
// Reviewer defaults must survive
|
|
121
|
+
expect(config.binaries.reviewer.pty).toBe(false);
|
|
122
|
+
expect(config.binaries.reviewer.heartbeat_timeout_ms).toBe(120000);
|
|
123
|
+
|
|
124
|
+
// Top-level defaults must survive
|
|
125
|
+
expect(config.convergence.score_threshold).toBe(0.9);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('partial nested config correctly inherits all non-provided fields', () => {
|
|
129
|
+
writeConfig(root, {
|
|
130
|
+
convergence: { score_threshold: 0.95 },
|
|
131
|
+
});
|
|
132
|
+
const { config } = loadConfig(root);
|
|
133
|
+
expect(config.convergence.score_threshold).toBe(0.95);
|
|
134
|
+
expect(config.convergence.max_iterations).toBe(3); // default preserved
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('invalid config — wrong type for score_threshold — throws NomosError with field path', () => {
|
|
138
|
+
writeConfig(root, { convergence: { score_threshold: 'not-a-number' } });
|
|
139
|
+
expect(() => loadConfig(root)).toThrowError(
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
code: 'config_invalid',
|
|
142
|
+
message: expect.stringContaining('convergence.score_threshold'),
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('missing config throws NomosError with config_not_found code', () => {
|
|
148
|
+
// No .nomos-config.json in root
|
|
149
|
+
expect(() => loadConfig(root)).toThrowError(
|
|
150
|
+
expect.objectContaining({ code: 'config_not_found' }),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('NomosError is instanceof Error', () => {
|
|
155
|
+
try {
|
|
156
|
+
loadConfig(root);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
expect(err).toBeInstanceOf(Error);
|
|
159
|
+
expect(err).toBeInstanceOf(NomosError);
|
|
160
|
+
expect((err as NomosError).name).toBe('NomosError');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('getDefaultConfig', () => {
|
|
166
|
+
it('returns a fully populated config with all fields', () => {
|
|
167
|
+
const cfg = getDefaultConfig();
|
|
168
|
+
|
|
169
|
+
expect(cfg.execution.default_mode).toBe('supervised');
|
|
170
|
+
expect(cfg.execution.shadow_branch_prefix).toBe('nomos/');
|
|
171
|
+
expect(cfg.execution.worktree_base).toBe('/tmp/nomos-worktrees/');
|
|
172
|
+
expect(cfg.binaries.planner.cmd).toBe('claude');
|
|
173
|
+
expect(cfg.binaries.planner.pty).toBe(true);
|
|
174
|
+
expect(cfg.binaries.reviewer.cmd).toBe('codex');
|
|
175
|
+
expect(cfg.binaries.reviewer.pty).toBe(false);
|
|
176
|
+
expect(cfg.convergence.score_threshold).toBe(0.9);
|
|
177
|
+
expect(cfg.convergence.max_iterations).toBe(3);
|
|
178
|
+
expect(cfg.budget.max_tokens_per_task).toBe(100000);
|
|
179
|
+
expect(cfg.security.redaction_label).toBe('[REDACTED]');
|
|
180
|
+
expect(cfg.git.auto_commit).toBe(true);
|
|
181
|
+
expect(cfg.review.max_context_files).toBe(5);
|
|
182
|
+
expect(cfg.logging.level).toBe('info');
|
|
183
|
+
expect(cfg.logging.retain_days).toBe(30);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('cost_per_1k_tokens contains expected model entries', () => {
|
|
187
|
+
const cfg = getDefaultConfig();
|
|
188
|
+
expect(cfg.budget.cost_per_1k_tokens['claude']).toEqual({ input: 0.003, output: 0.015 });
|
|
189
|
+
expect(cfg.budget.cost_per_1k_tokens['codex']).toEqual({ input: 0.0005, output: 0.002 });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { resolveBinary } from '../preflight.js';
|
|
4
|
+
import { NomosError } from '../errors.js';
|
|
5
|
+
|
|
6
|
+
describe('resolveBinary', () => {
|
|
7
|
+
it('resolves "node" to an absolute path', async () => {
|
|
8
|
+
const resolved = await resolveBinary('node');
|
|
9
|
+
expect(path.isAbsolute(resolved)).toBe(true);
|
|
10
|
+
expect(resolved).toContain('node');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('throws binary_not_found for a nonexistent binary', async () => {
|
|
14
|
+
await expect(resolveBinary('nonexistent-binary-xyz')).rejects.toMatchObject({
|
|
15
|
+
code: 'binary_not_found',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('throws binary_not_found for an absolute path that does not exist', async () => {
|
|
20
|
+
await expect(resolveBinary('/nonexistent/path/to/binary')).rejects.toMatchObject({
|
|
21
|
+
code: 'binary_not_found',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { assemblePrompt, assembleReviewPrompt, loadRules } from '../prompt.js';
|
|
6
|
+
import { readArchitecturalConstraints } from '../graph/constraints.js';
|
|
7
|
+
import type { PromptOptions, ReviewPromptOptions } from '../../types/index.js';
|
|
8
|
+
|
|
9
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
let tmpDir: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nomos-prompt-test-'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function scaffoldRules(opts: {
|
|
22
|
+
global?: string;
|
|
23
|
+
backend?: string;
|
|
24
|
+
session?: { taskId: string; content: string };
|
|
25
|
+
}): void {
|
|
26
|
+
const rulesDir = path.join(tmpDir, 'tasks-management', 'rules');
|
|
27
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
28
|
+
if (opts.global !== undefined) {
|
|
29
|
+
fs.writeFileSync(path.join(rulesDir, 'global.md'), opts.global);
|
|
30
|
+
}
|
|
31
|
+
if (opts.backend !== undefined) {
|
|
32
|
+
fs.writeFileSync(path.join(rulesDir, 'backend.md'), opts.backend);
|
|
33
|
+
}
|
|
34
|
+
if (opts.session) {
|
|
35
|
+
const sessionDir = path.join(rulesDir, 'session');
|
|
36
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
37
|
+
fs.writeFileSync(path.join(sessionDir, `${opts.session.taskId}.md`), opts.session.content);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const BASE_PROMPT_OPTIONS: PromptOptions = {
|
|
42
|
+
globalRules: 'Global rule content',
|
|
43
|
+
domainRules: 'Domain rule content',
|
|
44
|
+
sessionRules: null,
|
|
45
|
+
taskBody: 'Implement the feature',
|
|
46
|
+
contextFiles: [],
|
|
47
|
+
previousFeedback: null,
|
|
48
|
+
previousVersion: null,
|
|
49
|
+
mode: 'supervised',
|
|
50
|
+
architecturalConstraints: null,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ── assemblePrompt ────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
describe('assemblePrompt', () => {
|
|
56
|
+
it('includes all sections in correct order', () => {
|
|
57
|
+
const result = assemblePrompt(BASE_PROMPT_OPTIONS);
|
|
58
|
+
|
|
59
|
+
const systemIdx = result.indexOf('[SYSTEM RULES]');
|
|
60
|
+
const domainIdx = result.indexOf('[DOMAIN RULES]');
|
|
61
|
+
const taskIdx = result.indexOf('[TASK REQUIREMENTS]');
|
|
62
|
+
const instrIdx = result.indexOf('[INSTRUCTION]');
|
|
63
|
+
|
|
64
|
+
expect(systemIdx).toBeGreaterThanOrEqual(0);
|
|
65
|
+
expect(domainIdx).toBeGreaterThan(systemIdx);
|
|
66
|
+
expect(taskIdx).toBeGreaterThan(domainIdx);
|
|
67
|
+
expect(instrIdx).toBeGreaterThan(taskIdx);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('omits [DOMAIN RULES] when domainRules is empty', () => {
|
|
71
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, domainRules: '' });
|
|
72
|
+
expect(result).not.toContain('[DOMAIN RULES]');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('omits [SESSION CONSTRAINTS] when sessionRules is null', () => {
|
|
76
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, sessionRules: null });
|
|
77
|
+
expect(result).not.toContain('[SESSION CONSTRAINTS]');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('includes [SESSION CONSTRAINTS] when sessionRules is set', () => {
|
|
81
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, sessionRules: 'Only use TypeScript.' });
|
|
82
|
+
expect(result).toContain('[SESSION CONSTRAINTS]');
|
|
83
|
+
expect(result).toContain('Only use TypeScript.');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('includes [CONTEXT FILES] section when contextFiles is non-empty', () => {
|
|
87
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, contextFiles: ['src/auth.ts'] });
|
|
88
|
+
expect(result).toContain('[CONTEXT FILES]');
|
|
89
|
+
expect(result).toContain('- src/auth.ts');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('omits [CONTEXT FILES] section when contextFiles is empty', () => {
|
|
93
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, contextFiles: [] });
|
|
94
|
+
expect(result).not.toContain('[CONTEXT FILES]');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('formats previous feedback as a bullet list with version number', () => {
|
|
98
|
+
const result = assemblePrompt({
|
|
99
|
+
...BASE_PROMPT_OPTIONS,
|
|
100
|
+
previousFeedback: [
|
|
101
|
+
{ severity: 'high', category: 'security', description: 'SQL injection', suggestion: 'Use parameterized queries' },
|
|
102
|
+
],
|
|
103
|
+
previousVersion: 2,
|
|
104
|
+
});
|
|
105
|
+
expect(result).toContain('[PREVIOUS REVIEW FEEDBACK]');
|
|
106
|
+
expect(result).toContain('v2');
|
|
107
|
+
expect(result).toContain('- [high] SQL injection: Use parameterized queries');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('omits [PREVIOUS REVIEW FEEDBACK] when previousFeedback is null', () => {
|
|
111
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, previousFeedback: null });
|
|
112
|
+
expect(result).not.toContain('[PREVIOUS REVIEW FEEDBACK]');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('omits [PREVIOUS REVIEW FEEDBACK] when previousFeedback is empty array', () => {
|
|
116
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, previousFeedback: [] });
|
|
117
|
+
expect(result).not.toContain('[PREVIOUS REVIEW FEEDBACK]');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── assembleReviewPrompt ──────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe('assembleReviewPrompt', () => {
|
|
124
|
+
const BASE_REVIEW_OPTIONS: ReviewPromptOptions = {
|
|
125
|
+
planDiff: '+ added line\n- removed line',
|
|
126
|
+
planSummary: null,
|
|
127
|
+
globalRules: 'No secrets in output',
|
|
128
|
+
domainRules: '',
|
|
129
|
+
mode: 'supervised',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
it('includes the full JSON schema in [INSTRUCTION]', () => {
|
|
133
|
+
const result = assembleReviewPrompt(BASE_REVIEW_OPTIONS);
|
|
134
|
+
expect(result).toContain('[INSTRUCTION]');
|
|
135
|
+
expect(result).toContain('"score"');
|
|
136
|
+
expect(result).toContain('"summary"');
|
|
137
|
+
expect(result).toContain('"issues"');
|
|
138
|
+
expect(result).toContain('"severity"');
|
|
139
|
+
expect(result).toContain('"category"');
|
|
140
|
+
expect(result).toContain('"description"');
|
|
141
|
+
expect(result).toContain('"suggestion"');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('includes [ZERO-TOLERANCE CLAUSE] in auto mode', () => {
|
|
145
|
+
const result = assembleReviewPrompt({ ...BASE_REVIEW_OPTIONS, mode: 'auto' });
|
|
146
|
+
expect(result).toContain('[ZERO-TOLERANCE CLAUSE]');
|
|
147
|
+
expect(result).toContain('auto mode');
|
|
148
|
+
expect(result).toContain('score below 0.5');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('omits [ZERO-TOLERANCE CLAUSE] in supervised mode', () => {
|
|
152
|
+
const result = assembleReviewPrompt({ ...BASE_REVIEW_OPTIONS, mode: 'supervised' });
|
|
153
|
+
expect(result).not.toContain('[ZERO-TOLERANCE CLAUSE]');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('includes [AFFECTED FILES] when affectedFileSnippets is provided', () => {
|
|
157
|
+
const result = assembleReviewPrompt({
|
|
158
|
+
...BASE_REVIEW_OPTIONS,
|
|
159
|
+
affectedFileSnippets: [{ file: 'src/auth.ts', snippet: 'const x = 1;' }],
|
|
160
|
+
});
|
|
161
|
+
expect(result).toContain('[AFFECTED FILES]');
|
|
162
|
+
expect(result).toContain('// src/auth.ts');
|
|
163
|
+
expect(result).toContain('const x = 1;');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('omits [AFFECTED FILES] when affectedFileSnippets is absent', () => {
|
|
167
|
+
const result = assembleReviewPrompt(BASE_REVIEW_OPTIONS);
|
|
168
|
+
expect(result).not.toContain('[AFFECTED FILES]');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── architecturalConstraints injection (Step 5.6) ────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe('assemblePrompt — architectural constraints', () => {
|
|
175
|
+
it('includes [ARCHITECTURAL CONSTRAINTS] when architecturalConstraints is non-null', () => {
|
|
176
|
+
const constraints = '- src/types.ts → symbol: User\n Consumed by: src/services/user-service.ts\n Contract: "Manages user persistence"';
|
|
177
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, architecturalConstraints: constraints });
|
|
178
|
+
expect(result).toContain('[ARCHITECTURAL CONSTRAINTS]');
|
|
179
|
+
expect(result).toContain('src/types.ts → symbol: User');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('omits [ARCHITECTURAL CONSTRAINTS] when architecturalConstraints is null', () => {
|
|
183
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, architecturalConstraints: null });
|
|
184
|
+
expect(result).not.toContain('[ARCHITECTURAL CONSTRAINTS]');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('[ARCHITECTURAL CONSTRAINTS] appears before [INSTRUCTION]', () => {
|
|
188
|
+
const constraints = '- src/types.ts → symbol: User\n Consumed by: src/services/user-service.ts\n Contract: "Manages user persistence"';
|
|
189
|
+
const result = assemblePrompt({ ...BASE_PROMPT_OPTIONS, architecturalConstraints: constraints });
|
|
190
|
+
const constraintsIdx = result.indexOf('[ARCHITECTURAL CONSTRAINTS]');
|
|
191
|
+
const instrIdx = result.indexOf('[INSTRUCTION]');
|
|
192
|
+
expect(constraintsIdx).toBeGreaterThanOrEqual(0);
|
|
193
|
+
expect(instrIdx).toBeGreaterThan(constraintsIdx);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ── readArchitecturalConstraints integration (Step 5.6) ──────────────────────
|
|
198
|
+
|
|
199
|
+
describe('readArchitecturalConstraints', () => {
|
|
200
|
+
function writeMap(dir: string, data: unknown): void {
|
|
201
|
+
const graphDir = path.join(dir, 'tasks-management/graph');
|
|
202
|
+
fs.mkdirSync(graphDir, { recursive: true });
|
|
203
|
+
fs.writeFileSync(path.join(graphDir, 'project_map.json'), JSON.stringify(data));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function makeMinimalMap(files: Record<string, unknown>) {
|
|
207
|
+
return {
|
|
208
|
+
schema_version: 1,
|
|
209
|
+
generated_at: new Date().toISOString(),
|
|
210
|
+
root: '/tmp',
|
|
211
|
+
files,
|
|
212
|
+
stats: { total_files: Object.keys(files).length, total_symbols: 0, total_edges: 0, core_modules: [], structural_only: 0, semantically_enriched: 0, indexed: 0 },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
it('returns null when project_map.json does not exist', async () => {
|
|
217
|
+
const outputDir = path.join(tmpDir, 'tasks-management/graph');
|
|
218
|
+
const result = await readArchitecturalConstraints(tmpDir, outputDir, ['src/types.ts']);
|
|
219
|
+
expect(result).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns null when no context files have enriched dependents', async () => {
|
|
223
|
+
const outputDir = path.join(tmpDir, 'tasks-management/graph');
|
|
224
|
+
const map = makeMinimalMap({
|
|
225
|
+
'src/types.ts': {
|
|
226
|
+
file: 'src/types.ts', hash: 'sha256:abc', language: 'typescript',
|
|
227
|
+
symbols: [{ name: 'User', kind: 'interface', line: 1, end_line: 5, signature: 'interface User', exported: true }],
|
|
228
|
+
imports: [], dependents: [], dependencies: [], depth: 2,
|
|
229
|
+
last_parsed_at: new Date().toISOString(), semantic: null,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
writeMap(tmpDir, map);
|
|
233
|
+
const result = await readArchitecturalConstraints(tmpDir, outputDir, ['src/types.ts']);
|
|
234
|
+
expect(result).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('builds constraint string when enriched dependents consume exported symbols', async () => {
|
|
238
|
+
const outputDir = path.join(tmpDir, 'tasks-management/graph');
|
|
239
|
+
const map = makeMinimalMap({
|
|
240
|
+
'src/types.ts': {
|
|
241
|
+
file: 'src/types.ts', hash: 'sha256:abc', language: 'typescript',
|
|
242
|
+
symbols: [{ name: 'User', kind: 'interface', line: 1, end_line: 5, signature: 'interface User { id: string }', exported: true }],
|
|
243
|
+
imports: [], dependents: ['src/services/user-service.ts'], dependencies: [], depth: 2,
|
|
244
|
+
last_parsed_at: new Date().toISOString(), semantic: null,
|
|
245
|
+
},
|
|
246
|
+
'src/services/user-service.ts': {
|
|
247
|
+
file: 'src/services/user-service.ts', hash: 'sha256:def', language: 'typescript',
|
|
248
|
+
symbols: [],
|
|
249
|
+
imports: [{ source: '../types', resolved: 'src/types.ts', symbols: ['User'], is_external: false }],
|
|
250
|
+
dependents: [], dependencies: ['src/types.ts'], depth: 1,
|
|
251
|
+
last_parsed_at: new Date().toISOString(),
|
|
252
|
+
semantic: {
|
|
253
|
+
overview: 'User management service',
|
|
254
|
+
purpose: 'Manages user persistence and retrieval',
|
|
255
|
+
key_logic: [], usage_context: [],
|
|
256
|
+
source_hash: 'sha256:def',
|
|
257
|
+
enriched_at: new Date().toISOString(),
|
|
258
|
+
model: 'gemini',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
writeMap(tmpDir, map);
|
|
263
|
+
const result = await readArchitecturalConstraints(tmpDir, outputDir, ['src/types.ts']);
|
|
264
|
+
expect(result).not.toBeNull();
|
|
265
|
+
expect(result).toContain('src/types.ts');
|
|
266
|
+
expect(result).toContain('User');
|
|
267
|
+
expect(result).toContain('Consumed by:');
|
|
268
|
+
expect(result).toContain('src/services/user-service.ts');
|
|
269
|
+
expect(result).toContain('Contract:');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('[AMB-7] normalizes relative context file path correctly', async () => {
|
|
273
|
+
const outputDir = path.join(tmpDir, 'tasks-management/graph');
|
|
274
|
+
const map = makeMinimalMap({
|
|
275
|
+
'src/types.ts': {
|
|
276
|
+
file: 'src/types.ts', hash: 'sha256:abc', language: 'typescript',
|
|
277
|
+
symbols: [{ name: 'User', kind: 'interface', line: 1, end_line: 5, signature: 'interface User', exported: true }],
|
|
278
|
+
imports: [], dependents: ['src/services/user-service.ts'], dependencies: [], depth: 2,
|
|
279
|
+
last_parsed_at: new Date().toISOString(), semantic: null,
|
|
280
|
+
},
|
|
281
|
+
'src/services/user-service.ts': {
|
|
282
|
+
file: 'src/services/user-service.ts', hash: 'sha256:def', language: 'typescript',
|
|
283
|
+
symbols: [],
|
|
284
|
+
imports: [{ source: '../types', resolved: 'src/types.ts', symbols: ['User'], is_external: false }],
|
|
285
|
+
dependents: [], dependencies: ['src/types.ts'], depth: 1,
|
|
286
|
+
last_parsed_at: new Date().toISOString(),
|
|
287
|
+
semantic: {
|
|
288
|
+
overview: 'User management service',
|
|
289
|
+
purpose: 'Manages user persistence and retrieval',
|
|
290
|
+
key_logic: [], usage_context: [],
|
|
291
|
+
source_hash: 'sha256:def',
|
|
292
|
+
enriched_at: new Date().toISOString(),
|
|
293
|
+
model: 'gemini',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
writeMap(tmpDir, map);
|
|
298
|
+
|
|
299
|
+
// Pass a relative path that resolves to 'src/types.ts'
|
|
300
|
+
const result = await readArchitecturalConstraints(
|
|
301
|
+
tmpDir, outputDir,
|
|
302
|
+
['./src/types.ts'], // relative form
|
|
303
|
+
);
|
|
304
|
+
expect(result).not.toBeNull();
|
|
305
|
+
expect(result).toContain('src/types.ts');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('returns null and skips missing-from-map context files', async () => {
|
|
309
|
+
const outputDir = path.join(tmpDir, 'tasks-management/graph');
|
|
310
|
+
const map = makeMinimalMap({});
|
|
311
|
+
writeMap(tmpDir, map);
|
|
312
|
+
// File not in map
|
|
313
|
+
const result = await readArchitecturalConstraints(tmpDir, outputDir, ['src/nonexistent.ts']);
|
|
314
|
+
expect(result).toBeNull();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── loadRules ─────────────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
describe('loadRules', () => {
|
|
321
|
+
it('computes consistent hash for same input', async () => {
|
|
322
|
+
scaffoldRules({ global: 'global rules content', backend: 'backend rules' });
|
|
323
|
+
const r1 = await loadRules(tmpDir, 'task-1');
|
|
324
|
+
const r2 = await loadRules(tmpDir, 'task-1');
|
|
325
|
+
expect(r1.rulesHash).toBe(r2.rulesHash);
|
|
326
|
+
expect(r1.rulesHash).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('throws rules_missing when global.md is absent', async () => {
|
|
330
|
+
scaffoldRules({}); // no global.md
|
|
331
|
+
await expect(loadRules(tmpDir, 'task-1')).rejects.toMatchObject({
|
|
332
|
+
code: 'rules_missing',
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('handles missing backend.md gracefully (empty domain, no throw)', async () => {
|
|
337
|
+
scaffoldRules({ global: 'global content' }); // no backend.md
|
|
338
|
+
const result = await loadRules(tmpDir, 'task-1');
|
|
339
|
+
expect(result.domain).toBe('');
|
|
340
|
+
expect(result.rulesList).not.toContain('backend.md');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('includes session rules when session/{taskId}.md exists', async () => {
|
|
344
|
+
scaffoldRules({
|
|
345
|
+
global: 'global content',
|
|
346
|
+
session: { taskId: 'task-abc', content: 'session specific rules' },
|
|
347
|
+
});
|
|
348
|
+
const result = await loadRules(tmpDir, 'task-abc');
|
|
349
|
+
expect(result.session).toBe('session specific rules');
|
|
350
|
+
expect(result.rulesList).toContain('session/task-abc.md');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('returns null for session when session file is absent', async () => {
|
|
354
|
+
scaffoldRules({ global: 'global content' });
|
|
355
|
+
const result = await loadRules(tmpDir, 'task-xyz');
|
|
356
|
+
expect(result.session).toBeNull();
|
|
357
|
+
});
|
|
358
|
+
});
|