@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,195 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface ResolveResult {
|
|
9
|
+
resolved: string | null;
|
|
10
|
+
is_external: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─── ImportResolver ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolves import sources to absolute project-relative paths.
|
|
17
|
+
*
|
|
18
|
+
* [AMB-4 FIX] Uses a `knownFiles: Set<string>` contract for all existence
|
|
19
|
+
* checks — zero filesystem calls (no fs.existsSync, fs.access, fs.stat).
|
|
20
|
+
*
|
|
21
|
+
* [WATCH-4] The caller MUST populate `knownFiles` from BOTH ScanResult.files
|
|
22
|
+
* (newly scanned) AND ScanResult.carried (unchanged from previous run) before
|
|
23
|
+
* any resolve() call. Carried files that are invisible to the set will silently
|
|
24
|
+
* fail to resolve.
|
|
25
|
+
*
|
|
26
|
+
* The `async` signature exists for interface consistency — the implementation
|
|
27
|
+
* is synchronous.
|
|
28
|
+
*/
|
|
29
|
+
export class ImportResolver {
|
|
30
|
+
/** Tsconfig path aliases: key pattern → array of substitution templates */
|
|
31
|
+
private readonly aliasMap: Map<string, string[]>;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly projectRoot: string,
|
|
35
|
+
private readonly logger: { warn(msg: string): void },
|
|
36
|
+
) {
|
|
37
|
+
this.aliasMap = this.loadTsconfigPaths();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* [AMB-4 FIX] async for interface consistency; synchronous implementation.
|
|
42
|
+
* All existence checks use `knownFiles.has()` — NO filesystem calls.
|
|
43
|
+
*
|
|
44
|
+
* Resolution order:
|
|
45
|
+
* 1. External check (no leading . / / or matched alias → external)
|
|
46
|
+
* 2. Tsconfig alias substitution → continue to relative resolution
|
|
47
|
+
* 3. Relative resolution with extension probing against knownFiles
|
|
48
|
+
* 4. Path traversal guard (outside projectRoot → external)
|
|
49
|
+
* 5. Unresolvable → warn, return { resolved: null, is_external: false }
|
|
50
|
+
*/
|
|
51
|
+
async resolve(
|
|
52
|
+
importSource: string,
|
|
53
|
+
importerFile: string,
|
|
54
|
+
knownFiles: Set<string>,
|
|
55
|
+
): Promise<ResolveResult> {
|
|
56
|
+
// ── 1. External check ──────────────────────────────────────────────────
|
|
57
|
+
const isRelativeOrAbsolute =
|
|
58
|
+
importSource.startsWith('.') || importSource.startsWith('/');
|
|
59
|
+
const aliasMatch = this.matchAlias(importSource);
|
|
60
|
+
|
|
61
|
+
if (!isRelativeOrAbsolute && aliasMatch === null) {
|
|
62
|
+
return { resolved: null, is_external: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 3. Relative resolution ────────────────────────────────────────────
|
|
66
|
+
const importerDir = path.dirname(path.join(this.projectRoot, importerFile));
|
|
67
|
+
|
|
68
|
+
if (aliasMatch !== null) {
|
|
69
|
+
// ── 2. Tsconfig alias substitution ──────────────────────────────────
|
|
70
|
+
// Tsconfig path values are relative to projectRoot (the tsconfig dir).
|
|
71
|
+
for (const substituted of aliasMatch) {
|
|
72
|
+
const result = this.resolveRelative(substituted, this.projectRoot, knownFiles);
|
|
73
|
+
if (result !== null) return result;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Regular relative import — resolve from importer directory
|
|
77
|
+
const result = this.resolveRelative(importSource, importerDir, knownFiles);
|
|
78
|
+
if (result !== null) return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── 5. Unresolvable ───────────────────────────────────────────────────
|
|
82
|
+
this.logger.warn(
|
|
83
|
+
`[nomos:graph:warn] Cannot resolve import '${importSource}' from '${importerFile}'`,
|
|
84
|
+
);
|
|
85
|
+
return { resolved: null, is_external: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Attempt to resolve a (possibly alias-substituted) source string relative
|
|
92
|
+
* to the importer directory.
|
|
93
|
+
*
|
|
94
|
+
* Probes candidate paths in order against `knownFiles`.
|
|
95
|
+
* [AMB-4] NO filesystem calls — knownFiles.has() only.
|
|
96
|
+
*/
|
|
97
|
+
private resolveRelative(
|
|
98
|
+
src: string,
|
|
99
|
+
importerDir: string,
|
|
100
|
+
knownFiles: Set<string>,
|
|
101
|
+
): ResolveResult | null {
|
|
102
|
+
const extensions = ['.ts', '.tsx', '.js'];
|
|
103
|
+
const indexFiles = ['index.ts', 'index.tsx', 'index.js'];
|
|
104
|
+
|
|
105
|
+
// Compute base absolute path
|
|
106
|
+
const absBase = path.resolve(importerDir, src);
|
|
107
|
+
|
|
108
|
+
// ── 4. Path traversal guard ───────────────────────────────────────────
|
|
109
|
+
const rel = path.relative(this.projectRoot, absBase);
|
|
110
|
+
if (rel.startsWith('..')) {
|
|
111
|
+
this.logger.warn(
|
|
112
|
+
`[nomos:graph:warn] Import path traversal outside projectRoot: '${src}'`,
|
|
113
|
+
);
|
|
114
|
+
return { resolved: null, is_external: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const candidates: string[] = [];
|
|
118
|
+
|
|
119
|
+
// Direct hit (if src already has extension)
|
|
120
|
+
candidates.push(absBase);
|
|
121
|
+
|
|
122
|
+
// Extension probing
|
|
123
|
+
for (const ext of extensions) {
|
|
124
|
+
candidates.push(`${absBase}${ext}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Index file resolution
|
|
128
|
+
for (const idx of indexFiles) {
|
|
129
|
+
candidates.push(path.join(absBase, idx));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
const relCandidate = path.relative(this.projectRoot, candidate);
|
|
134
|
+
if (knownFiles.has(relCandidate)) {
|
|
135
|
+
return { resolved: relCandidate, is_external: false };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Match importSource against tsconfig `compilerOptions.paths` aliases.
|
|
144
|
+
* Returns substituted source strings if matched, null otherwise.
|
|
145
|
+
*/
|
|
146
|
+
private matchAlias(importSource: string): string[] | null {
|
|
147
|
+
for (const [pattern, substitutions] of this.aliasMap) {
|
|
148
|
+
if (pattern.endsWith('/*')) {
|
|
149
|
+
// Wildcard alias: @/utils/* → src/utils/*
|
|
150
|
+
const prefix = pattern.slice(0, -2); // remove trailing /*
|
|
151
|
+
if (importSource.startsWith(prefix + '/') || importSource === prefix) {
|
|
152
|
+
const capture = importSource.startsWith(prefix + '/')
|
|
153
|
+
? importSource.slice(prefix.length + 1)
|
|
154
|
+
: '';
|
|
155
|
+
return substitutions.map((sub) =>
|
|
156
|
+
sub.endsWith('/*') ? sub.slice(0, -2) + '/' + capture : sub,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// Exact alias
|
|
161
|
+
if (importSource === pattern) {
|
|
162
|
+
return [...substitutions];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Read `tsconfig.json` at projectRoot and extract `compilerOptions.paths`.
|
|
171
|
+
* Missing or malformed → returns empty map (graceful degradation).
|
|
172
|
+
*/
|
|
173
|
+
private loadTsconfigPaths(): Map<string, string[]> {
|
|
174
|
+
const tsconfigPath = path.join(this.projectRoot, 'tsconfig.json');
|
|
175
|
+
try {
|
|
176
|
+
const raw = fs.readFileSync(tsconfigPath, 'utf-8');
|
|
177
|
+
const tsconfig = JSON.parse(raw) as {
|
|
178
|
+
compilerOptions?: { paths?: Record<string, string[]> };
|
|
179
|
+
};
|
|
180
|
+
const paths = tsconfig.compilerOptions?.paths;
|
|
181
|
+
if (!paths || typeof paths !== 'object') return new Map();
|
|
182
|
+
|
|
183
|
+
const map = new Map<string, string[]>();
|
|
184
|
+
for (const [key, values] of Object.entries(paths)) {
|
|
185
|
+
if (Array.isArray(values)) {
|
|
186
|
+
map.set(key, values as string[]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return map;
|
|
190
|
+
} catch {
|
|
191
|
+
// Missing tsconfig or parse error → empty alias map
|
|
192
|
+
return new Map();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import type { NomosConfig } from '../../types/index.js';
|
|
6
|
+
import type { FileNode, ProjectMap } from '../../types/index.js';
|
|
7
|
+
|
|
8
|
+
// ─── Language Map ─────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const LANGUAGE_MAP: Record<string, string> = {
|
|
11
|
+
'.ts': 'typescript',
|
|
12
|
+
'.tsx': 'tsx',
|
|
13
|
+
'.mts': 'typescript',
|
|
14
|
+
'.cts': 'typescript',
|
|
15
|
+
'.d.ts': 'typescript',
|
|
16
|
+
'.js': 'javascript',
|
|
17
|
+
'.mjs': 'javascript',
|
|
18
|
+
'.cjs': 'javascript',
|
|
19
|
+
'.jsx': 'jsx',
|
|
20
|
+
'.py': 'python',
|
|
21
|
+
'.go': 'go',
|
|
22
|
+
'.rs': 'rust',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ─── ScanResult ───────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface ScanResult {
|
|
28
|
+
files: Map<string, { file: string; hash: string; language: string; content: string }>;
|
|
29
|
+
carried: Map<string, FileNode>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── FileScanner ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export class FileScanner {
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly projectRoot: string,
|
|
37
|
+
private readonly config: NomosConfig['graph'],
|
|
38
|
+
private readonly logger: { warn(msg: string): void; info(msg: string): void },
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
async scan(
|
|
42
|
+
existingMap: ProjectMap | null,
|
|
43
|
+
force: boolean,
|
|
44
|
+
globPatterns?: string[],
|
|
45
|
+
): Promise<ScanResult> {
|
|
46
|
+
const ignorePatterns = await this.buildIgnorePatterns();
|
|
47
|
+
|
|
48
|
+
const patterns = globPatterns ?? ['**/*'];
|
|
49
|
+
const absolutePaths = await fg(patterns, {
|
|
50
|
+
cwd: this.projectRoot,
|
|
51
|
+
absolute: true,
|
|
52
|
+
dot: true,
|
|
53
|
+
ignore: ignorePatterns,
|
|
54
|
+
followSymbolicLinks: false,
|
|
55
|
+
onlyFiles: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result: ScanResult = {
|
|
59
|
+
files: new Map(),
|
|
60
|
+
carried: new Map(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const absolutePath of absolutePaths) {
|
|
64
|
+
const relPath = path.relative(this.projectRoot, absolutePath);
|
|
65
|
+
const ext = this.resolveExtension(relPath);
|
|
66
|
+
const language = LANGUAGE_MAP[ext];
|
|
67
|
+
|
|
68
|
+
if (!language) continue;
|
|
69
|
+
|
|
70
|
+
// [GAP-1 FIX] Skip files exceeding 500KB
|
|
71
|
+
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
|
72
|
+
try {
|
|
73
|
+
stat = await fs.stat(absolutePath);
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
76
|
+
if (code === 'ENOENT' || code === 'EACCES') {
|
|
77
|
+
this.logger.warn(`[nomos:graph:warn] Skipping ${relPath} — cannot stat file`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (stat.size > 512000) {
|
|
84
|
+
this.logger.warn(`[nomos:graph:warn] Skipping ${relPath} — exceeds 500KB size limit`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let content: string;
|
|
89
|
+
try {
|
|
90
|
+
content = await fs.readFile(absolutePath, 'utf-8');
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
93
|
+
if (code === 'ENOENT' || code === 'EACCES') {
|
|
94
|
+
this.logger.warn(`[nomos:graph:warn] Skipping ${relPath} — cannot read file`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hash = `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`;
|
|
101
|
+
|
|
102
|
+
// Incremental: carry forward if hash unchanged and not forced
|
|
103
|
+
if (!force && existingMap) {
|
|
104
|
+
const existing = existingMap.files[relPath];
|
|
105
|
+
if (existing && existing.hash === hash) {
|
|
106
|
+
result.carried.set(relPath, existing);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
result.files.set(relPath, { file: relPath, hash, language, content });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Private Helpers ──────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
private resolveExtension(filePath: string): string {
|
|
120
|
+
// Handle .d.ts before generic .ts
|
|
121
|
+
if (filePath.endsWith('.d.ts')) return '.d.ts';
|
|
122
|
+
return path.extname(filePath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async buildIgnorePatterns(): Promise<string[]> {
|
|
126
|
+
const patterns: string[] = [...this.config.exclude_patterns];
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const gitignoreContent = await fs.readFile(
|
|
130
|
+
path.join(this.projectRoot, '.gitignore'),
|
|
131
|
+
'utf-8',
|
|
132
|
+
);
|
|
133
|
+
for (const line of gitignoreContent.split('\n')) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
136
|
+
patterns.push(trimmed);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// No .gitignore — use empty list
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return patterns;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import { stripAnsi } from '../utils/ansi.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* createLogger — Winston logger with two transports:
|
|
6
|
+
*
|
|
7
|
+
* Console transport: colorized, format: [nomos:<level>] <message>
|
|
8
|
+
* File transport: writes to {logDir}/nomos.log, ANSI-stripped, JSON format.
|
|
9
|
+
*
|
|
10
|
+
* @param level - Winston log level (default: 'info')
|
|
11
|
+
* @param logDir - Directory for nomos.log. Omit to disable file transport.
|
|
12
|
+
*/
|
|
13
|
+
export function createLogger(level: string = 'info', logDir?: string): winston.Logger {
|
|
14
|
+
const transports: winston.transport[] = [
|
|
15
|
+
new winston.transports.Console({
|
|
16
|
+
format: winston.format.combine(
|
|
17
|
+
winston.format.colorize({ all: true }),
|
|
18
|
+
winston.format.printf(({ level: lvl, message }) => {
|
|
19
|
+
return `[nomos:${lvl}] ${String(message)}`;
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
}),
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (logDir) {
|
|
26
|
+
transports.push(
|
|
27
|
+
new winston.transports.File({
|
|
28
|
+
filename: `${logDir}/nomos.log`,
|
|
29
|
+
format: winston.format.combine(
|
|
30
|
+
// Strip ANSI codes before writing to file
|
|
31
|
+
winston.format((info) => {
|
|
32
|
+
info['message'] = stripAnsi(String(info['message']));
|
|
33
|
+
return info;
|
|
34
|
+
})(),
|
|
35
|
+
winston.format.timestamp(),
|
|
36
|
+
winston.format.json(),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return winston.createLogger({
|
|
43
|
+
level,
|
|
44
|
+
transports,
|
|
45
|
+
});
|
|
46
|
+
}
|