genesis-compiler 1.10.6 → 1.10.8
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/README.md +42 -2
- package/docs/prompt-integration.md +2 -2
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/profiles/engineering/baseline.md +1 -1
- package/src/cli.js +15 -6
- package/src/index/architecture-observations.js +94 -7
- package/src/index/architecture.js +21 -2
- package/src/index/call-web.js +419 -0
- package/src/index/code-index.js +154 -16
- package/src/index/code-indexers/ast-grep.js +453 -77
- package/src/index/code-indexers/npm-packages.js +4 -3
- package/src/index/code-indexers/worker.js +5 -2
- package/src/index/collaboration.js +37 -3
- package/src/index/entry-points.js +269 -0
- package/src/index/extraction-cache.js +81 -0
- package/src/index/project-files.js +7 -3
- package/src/index/project-inspection.js +5 -2
- package/src/index/prompt.js +1 -0
- package/src/index/utils.js +34 -0
- package/src/index.js +3 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { CALL_CERTAINTIES, CALL_METHODS, EXACT_CALL_METHODS, FUNCTION_KINDS, MAX_CALLEE_LENGTH, MAX_CALL_TARGETS, MAX_CALLS_PER_FILE, functionId } from './architecture-observations.js';
|
|
5
|
+
import { GenesisError } from './errors.js';
|
|
6
|
+
import { gitContext } from './git.js';
|
|
7
|
+
import { sha256 } from './utils.js';
|
|
8
|
+
|
|
9
|
+
export const CALL_WEB_SCHEMA = 'genesis.call-web.v1';
|
|
10
|
+
export const CALL_WEB_PATH = '.genesis/call-web.json';
|
|
11
|
+
const MAX_DIAGNOSTICS_PER_CODE = 100;
|
|
12
|
+
const RE_EXPORT_DEPTH = 4;
|
|
13
|
+
const REJOIN_LINES = 3;
|
|
14
|
+
|
|
15
|
+
// Language-level knowledge only: names every program in the language can use
|
|
16
|
+
// without declaring them. A project declaration of the same name wins.
|
|
17
|
+
const words = value => new Set(value.trim().split(/\s+/u));
|
|
18
|
+
const BUILTIN_GLOBALS = {
|
|
19
|
+
javascript: words(`console JSON Math Object Array String Number Boolean Promise Date RegExp Error TypeError RangeError Map Set WeakMap WeakSet Symbol Reflect Proxy Intl BigInt
|
|
20
|
+
Buffer process globalThis window document navigator location history localStorage sessionStorage performance crypto setTimeout setInterval clearTimeout clearInterval setImmediate
|
|
21
|
+
clearImmediate queueMicrotask requestAnimationFrame cancelAnimationFrame structuredClone fetch require parseInt parseFloat isNaN isFinite encodeURIComponent decodeURIComponent
|
|
22
|
+
encodeURI decodeURI atob btoa URL URLSearchParams TextEncoder TextDecoder AbortController AbortSignal Blob File FormData Headers Request Response Event CustomEvent EventTarget
|
|
23
|
+
Uint8Array Int8Array Uint16Array Int16Array Uint32Array Int32Array Float32Array Float64Array ArrayBuffer DataView WebSocket Worker alert confirm prompt eval import
|
|
24
|
+
describe it test expect beforeEach afterEach beforeAll afterAll vi jest`),
|
|
25
|
+
python: words(`print len range enumerate zip map filter sorted reversed sum min max abs any all isinstance issubclass hasattr getattr setattr delattr type super str int float bool
|
|
26
|
+
list dict set tuple frozenset bytes bytearray object open iter next repr hash id callable vars dir format round divmod pow chr ord hex oct bin input globals locals
|
|
27
|
+
staticmethod classmethod property compile eval exec memoryview slice __import__ breakpoint ascii aiter anext NotImplemented`),
|
|
28
|
+
go: words('len cap append make new panic recover print println copy delete close complex real imag min max clear string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte rune float32 float64 bool error any'),
|
|
29
|
+
rust: words('Some Ok Err None Box Vec String Option Result drop'),
|
|
30
|
+
java: words('System String Math Integer Long Double Float Boolean Character Byte Short Object List Map Set Arrays Collections Objects Optional Stream Thread'),
|
|
31
|
+
kotlin: words(`println print listOf mapOf setOf arrayOf mutableListOf mutableMapOf mutableSetOf emptyList emptyMap emptySet lazy require requireNotNull check checkNotNull error TODO repeat
|
|
32
|
+
run with apply also let takeIf maxOf minOf Pair Triple`),
|
|
33
|
+
csharp: words('Console Math String Convert Int32 Int64 Double Boolean Object Array List Dictionary Task Enumerable Guid DateTime TimeSpan Environment Path File Directory nameof typeof'),
|
|
34
|
+
php: words(`count strlen str_replace substr strpos stripos explode implode in_array is_array is_string is_int is_numeric is_null is_bool is_object isset unset empty json_encode
|
|
35
|
+
json_decode sprintf printf vsprintf var_dump print_r var_export trim ltrim rtrim strtolower strtoupper ucfirst lcfirst ucwords preg_match preg_match_all preg_replace preg_split
|
|
36
|
+
file_get_contents file_put_contents file_exists is_file is_dir mkdir date time strtotime intval floatval boolval strval array_key_exists array_keys array_values array_map
|
|
37
|
+
array_filter array_merge array_slice array_splice array_search array_unique array_reverse array_push array_pop array_shift array_unshift compact extract define defined
|
|
38
|
+
function_exists method_exists class_exists property_exists call_user_func call_user_func_array func_get_args sort usort ksort uasort max min abs round floor ceil rand
|
|
39
|
+
mt_rand random_int htmlspecialchars urlencode rawurlencode base64_encode base64_decode md5 sha1 hash uniqid microtime header http_response_code session_start die exit
|
|
40
|
+
get_class spl_autoload_register iterator_to_array sprintf number_format str_contains str_starts_with str_ends_with array_combine array_fill range`),
|
|
41
|
+
ruby: words(`puts print p pp require require_relative load raise fail attr_accessor attr_reader attr_writer include extend prepend private protected public module_function
|
|
42
|
+
lambda proc loop format sprintf rand sleep catch throw block_given? yield binding caller freeze Integer Float String Array Hash`),
|
|
43
|
+
cpp: words(`printf fprintf sprintf snprintf scanf malloc calloc realloc free memcpy memset memmove memcmp strcmp strncmp strlen strcpy strncpy strcat strchr strstr exit abort
|
|
44
|
+
assert fopen fclose fread fwrite fgets fputs puts getchar putchar atoi atof strtol sizeof static_cast dynamic_cast reinterpret_cast const_cast std`),
|
|
45
|
+
shell: words(''),
|
|
46
|
+
};
|
|
47
|
+
const BUILTIN_MEMBERS = {
|
|
48
|
+
javascript: words(`push pop shift unshift slice splice concat join map filter reduce reduceRight forEach find findIndex findLast findLastIndex some every includes indexOf lastIndexOf
|
|
49
|
+
sort reverse flat flatMap fill keys values entries has get set add delete clear then catch finally toString valueOf toLowerCase toUpperCase trim trimStart trimEnd split
|
|
50
|
+
replace replaceAll match matchAll startsWith endsWith padStart padEnd charAt charCodeAt codePointAt substring substr repeat at test exec call apply bind hasOwnProperty
|
|
51
|
+
toFixed toISOString toJSON getTime getFullYear assign freeze from of isArray stringify parse log warn error info debug resolve reject all allSettled race any max min floor
|
|
52
|
+
ceil round abs random now emit on once off addEventListener removeEventListener dispatchEvent querySelector querySelectorAll getElementById appendChild removeChild
|
|
53
|
+
setAttribute getAttribute preventDefault stopPropagation localeCompare normalize sqrt pow trunc sign`),
|
|
54
|
+
python: words(`append extend insert remove pop clear index count sort reverse copy keys values items get setdefault update join split rsplit strip lstrip rstrip replace
|
|
55
|
+
startswith endswith find rfind lower upper format encode decode read write readline readlines close group groups groupdict match search sub findall finditer add discard
|
|
56
|
+
splitlines partition rpartition isdigit isalpha isalnum isspace isupper islower title capitalize zfill ljust rjust center expandtabs translate fullmatch`),
|
|
57
|
+
go: words('Error String Lock Unlock RLock RUnlock Close Write Read Len Done Wait Add'),
|
|
58
|
+
rust: words(`unwrap expect clone to_string to_owned into iter iter_mut into_iter map collect push pop len is_empty as_str as_ref as_mut ok err unwrap_or unwrap_or_else
|
|
59
|
+
unwrap_or_default and_then or_else filter get get_mut insert remove contains contains_key keys values entry or_insert join split trim format chars bytes lines`),
|
|
60
|
+
java: words(`println print printf equals hashCode toString get put add remove size isEmpty contains containsKey stream map filter collect forEach append length charAt substring
|
|
61
|
+
trim split format valueOf parseInt of asList orElse ifPresent getClass compareTo`),
|
|
62
|
+
kotlin: words(`map filter forEach first firstOrNull last lastOrNull toList toMutableList toSet joinToString add remove get set contains isEmpty isNotEmpty size toString
|
|
63
|
+
equals hashCode let also apply run takeIf trim split substring format`),
|
|
64
|
+
csharp: words(`ToString Equals GetHashCode GetType Add Remove Contains ContainsKey Clear WriteLine Write ReadLine Format Select Where ToList ToArray First FirstOrDefault
|
|
65
|
+
Any All Count Sum OrderBy GroupBy Join Split Trim Replace Substring StartsWith EndsWith IndexOf ConfigureAwait`),
|
|
66
|
+
php: words(''),
|
|
67
|
+
ruby: words(`each map select reject find detect each_with_index each_with_object inject reduce to_s to_i to_f to_sym to_a to_h include? empty? nil? length size push pop
|
|
68
|
+
join split strip gsub sub upcase downcase new call keys values merge first last any? all? sort sort_by min max sum uniq compact flatten freeze dup`),
|
|
69
|
+
cpp: words(`push_back emplace_back pop_back size begin end cbegin cend c_str insert erase find at front back empty clear reserve resize data substr append count
|
|
70
|
+
first second get reset swap lock unlock`),
|
|
71
|
+
shell: words(''),
|
|
72
|
+
};
|
|
73
|
+
const SELF_RECEIVERS = { javascript: ['this'], python: ['self', 'cls'], php: ['$this', 'self', 'static'], ruby: ['self'], java: ['this'], kotlin: ['this'], csharp: ['this'], cpp: ['this'], rust: ['self', 'Self'], go: [], shell: [] };
|
|
74
|
+
const PARENT_RECEIVERS = new Set(['super', 'parent', 'base']);
|
|
75
|
+
// Statements that bind every top-level function of the included file.
|
|
76
|
+
const WILDCARD_LANGUAGES = new Set(['cpp', 'ruby', 'shell', 'php']);
|
|
77
|
+
const NPM_PACKAGE = /^(?:node:|(?:@[a-z0-9~][a-z0-9._~-]*\/)?[a-z0-9~][a-z0-9._~-]*(?:\/|$))/iu;
|
|
78
|
+
|
|
79
|
+
function externalSpecifier(language, specifier) {
|
|
80
|
+
if (!specifier) return false;
|
|
81
|
+
if (language === 'javascript') return !specifier.startsWith('.') && !specifier.startsWith('/') && NPM_PACKAGE.test(specifier);
|
|
82
|
+
if (language === 'python' || language === 'ruby') return !specifier.startsWith('.');
|
|
83
|
+
if (language === 'go') return !specifier.split('/')[0].includes('.');
|
|
84
|
+
if (language === 'rust') return /^(?:std|core|alloc)(?:::|$)/u.test(specifier);
|
|
85
|
+
if (language === 'java' || language === 'kotlin') return /^(?:java|javax|jakarta|kotlin|kotlinx|android|androidx)\./u.test(specifier);
|
|
86
|
+
if (language === 'csharp') return /^(?:System|Microsoft)(?:\.|$)/u.test(specifier);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const point = (line, column) => ({ line, column });
|
|
91
|
+
const comparePoints = (left, right) => left.line - right.line || left.column - right.column;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves cached per-file call-site syntax against one snapshot and joins
|
|
95
|
+
* adapter observations by the §3.1.6 precedence rules. The generic baseline is
|
|
96
|
+
* name and import scope only, so it never claims an exact edge.
|
|
97
|
+
*/
|
|
98
|
+
export function buildCallWeb({ architecture, extractions, machineFunctions = [], observations = null, sourceIdentity }) {
|
|
99
|
+
const diagnostics = [];
|
|
100
|
+
const counted = new Map();
|
|
101
|
+
const diagnose = (entry) => {
|
|
102
|
+
const seen = (counted.get(entry.code) || 0) + 1;
|
|
103
|
+
counted.set(entry.code, seen);
|
|
104
|
+
if (seen <= MAX_DIAGNOSTICS_PER_CODE) diagnostics.push(entry);
|
|
105
|
+
};
|
|
106
|
+
const files = architecture.files.map(file => ({ path: file.path, hash: file.hash, subsystemId: file.subsystemId || null, language: file.language, id: file.id }));
|
|
107
|
+
const filesByPath = new Map(files.map((file, index) => [file.path, { ...file, index }]));
|
|
108
|
+
const filesById = new Map(files.map(file => [file.id, file]));
|
|
109
|
+
const exported = new Set(machineFunctions.filter(entry => entry.visibility === 'public').map(entry => `${entry.path}#${entry.line}:${entry.column}`));
|
|
110
|
+
const adapterFunctions = (observations?.functions || []).filter(entry => filesByPath.has(entry.file));
|
|
111
|
+
const adapterOf = entry => entry.adapter || observations.adapter;
|
|
112
|
+
const adapterFiles = new Map();
|
|
113
|
+
for (const entry of adapterFunctions) if (!adapterFiles.has(entry.file)) adapterFiles.set(entry.file, adapterOf(entry));
|
|
114
|
+
|
|
115
|
+
// Generic function sites for every extracted file, including adapter files:
|
|
116
|
+
// calls elsewhere may target them before re-joining to adapter functions.
|
|
117
|
+
const generic = new Map();
|
|
118
|
+
for (const [filePath, sites] of extractions) {
|
|
119
|
+
const file = filesByPath.get(filePath);
|
|
120
|
+
if (!file) continue;
|
|
121
|
+
const functions = sites.functions.map(entry => ({
|
|
122
|
+
id: functionId(filePath, entry.start, entry.kind), file: filePath, name: entry.name, kind: entry.kind, start: entry.start, end: entry.end,
|
|
123
|
+
...(entry.container ? { container: entry.container } : {}),
|
|
124
|
+
...(exported.has(`${filePath}#${entry.start.line}:${entry.start.column}`) ? { exported: true } : {}),
|
|
125
|
+
parent: entry.parent, default: entry.default === true,
|
|
126
|
+
}));
|
|
127
|
+
const ids = new Set();
|
|
128
|
+
for (const entry of functions) { entry.duplicate = ids.has(entry.id); ids.add(entry.id); }
|
|
129
|
+
const moduleRecord = comparePoints(point(1, 1), sites.moduleEnd) < 0
|
|
130
|
+
? { id: functionId(filePath, point(1, 1), 'module'), file: filePath, name: '', kind: 'module', start: point(1, 1), end: sites.moduleEnd, parent: -1, default: false }
|
|
131
|
+
: null;
|
|
132
|
+
generic.set(filePath, { file, sites, functions, moduleRecord, language: file.language });
|
|
133
|
+
}
|
|
134
|
+
const projectNames = new Set([...generic.values()].flatMap(entry => entry.functions.map(fn => fn.name)).concat(adapterFunctions.map(entry => entry.name)).filter(Boolean));
|
|
135
|
+
|
|
136
|
+
// Per-file lookup tables used by the resolution rules.
|
|
137
|
+
for (const entry of generic.values()) {
|
|
138
|
+
entry.byName = new Map(); entry.topByName = new Map(); entry.members = new Map();
|
|
139
|
+
const add = (map, key, value) => { if (!map.has(key)) map.set(key, []); map.get(key).push(value); };
|
|
140
|
+
for (const fn of entry.functions) {
|
|
141
|
+
if (!fn.name || fn.duplicate) continue;
|
|
142
|
+
add(entry.byName, fn.name, fn);
|
|
143
|
+
if ((fn.parent < 0 && !fn.container) || fn.exported) add(entry.topByName, fn.name, fn);
|
|
144
|
+
if (fn.container && ['method', 'constructor'].includes(fn.kind)) add(entry.members, `${fn.container}\0${fn.name}`, fn);
|
|
145
|
+
}
|
|
146
|
+
entry.defaults = entry.functions.filter(fn => fn.default && !fn.duplicate);
|
|
147
|
+
}
|
|
148
|
+
const relationsByFile = new Map();
|
|
149
|
+
for (const relation of architecture.relations || []) {
|
|
150
|
+
const file = filesById.get(relation.fromFileId);
|
|
151
|
+
if (!file) continue;
|
|
152
|
+
if (!relationsByFile.has(file.path)) relationsByFile.set(file.path, []);
|
|
153
|
+
relationsByFile.get(file.path).push(relation);
|
|
154
|
+
}
|
|
155
|
+
const packagesById = new Map((architecture.packages || []).map(entry => [entry.id, entry]));
|
|
156
|
+
const relationTarget = relation => relation.resolution === 'resolved' && relation.targetFileId ? filesById.get(relation.targetFileId)?.path || null : null;
|
|
157
|
+
const relationExternal = (relation, language) => (relation.targetPackageId != null && packagesById.get(relation.targetPackageId)?.opaque !== false && !relation.targetFileId)
|
|
158
|
+
|| (relation.resolution !== 'resolved' && externalSpecifier(language, relation.specifier));
|
|
159
|
+
// An included C/C++ header declares what its same-named source file defines.
|
|
160
|
+
const headerSources = header => /\.(?:h|hh|hpp|hxx)$/u.test(header)
|
|
161
|
+
? ['.c', '.cc', '.cpp', '.cxx'].map(extension => header.replace(/\.[^.]+$/u, extension)).filter(candidate => filesByPath.has(candidate))
|
|
162
|
+
: [];
|
|
163
|
+
for (const [filePath, entry] of generic) {
|
|
164
|
+
const relations = relationsByFile.get(filePath) || [];
|
|
165
|
+
const atStart = new Map(relations.map(relation => [`${relation.start?.line}:${relation.start?.column}`, relation]));
|
|
166
|
+
entry.named = new Map(); entry.namespaces = new Map(); entry.wildcards = [];
|
|
167
|
+
for (const binding of entry.sites.bindings) {
|
|
168
|
+
const relation = atStart.get(`${binding.at.line}:${binding.at.column}`);
|
|
169
|
+
const resolved = { ...binding, target: relation ? relationTarget(relation) : null, external: relation ? relationExternal(relation, entry.language) : externalSpecifier(entry.language, null) };
|
|
170
|
+
if (binding.wildcard) entry.wildcards.push(resolved);
|
|
171
|
+
else (binding.namespace ? entry.namespaces : entry.named).set(binding.local, resolved);
|
|
172
|
+
}
|
|
173
|
+
const included = [];
|
|
174
|
+
if (WILDCARD_LANGUAGES.has(entry.language)) for (const relation of relations) {
|
|
175
|
+
const target = relationTarget(relation);
|
|
176
|
+
if (target) included.push(target, ...(entry.language === 'cpp' ? headerSources(target) : []));
|
|
177
|
+
}
|
|
178
|
+
for (const target of included) entry.wildcards.push({ target, external: false });
|
|
179
|
+
entry.imports = new Set([filePath, ...included, ...relations.filter(relation => !relation.typeOnly).map(relationTarget).filter(Boolean)]);
|
|
180
|
+
entry.reExports = relations.filter(relation => relation.kind === 're-export' && Array.isArray(relation.exports));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A name exported by T, following named bindings and re-exports a few hops.
|
|
184
|
+
function exportedFunctions(target, name, depth = 0, seen = new Set()) {
|
|
185
|
+
const entry = generic.get(target);
|
|
186
|
+
if (!entry || seen.has(target)) return [];
|
|
187
|
+
const own = entry.topByName.get(name);
|
|
188
|
+
if (own?.length) return own;
|
|
189
|
+
if (depth >= RE_EXPORT_DEPTH) return [];
|
|
190
|
+
seen.add(target);
|
|
191
|
+
const binding = entry.named.get(name);
|
|
192
|
+
if (binding?.target) return binding.imported === 'default' ? generic.get(binding.target)?.defaults || [] : exportedFunctions(binding.target, binding.imported, depth + 1, seen);
|
|
193
|
+
const found = [];
|
|
194
|
+
for (const relation of entry.reExports) {
|
|
195
|
+
const next = relationTarget(relation);
|
|
196
|
+
for (const exported of relation.exports) {
|
|
197
|
+
if (next && exported.name === '*') found.push(...exportedFunctions(next, name, depth + 1, seen));
|
|
198
|
+
else if (next && exported.alias === name) found.push(...exportedFunctions(next, exported.name, depth + 1, seen));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [...new Set(found)];
|
|
202
|
+
}
|
|
203
|
+
// A Python `from package import module` binds the submodule file.
|
|
204
|
+
function namespaceTarget(entry, receiver) {
|
|
205
|
+
const namespace = entry.namespaces.get(receiver);
|
|
206
|
+
if (namespace) return namespace;
|
|
207
|
+
const binding = entry.named.get(receiver);
|
|
208
|
+
if (entry.language !== 'python' || !binding?.target?.endsWith('__init__.py') || generic.get(binding.target)?.topByName.has(binding.imported)) return null;
|
|
209
|
+
const directory = path.posix.dirname(binding.target);
|
|
210
|
+
const module = [`${directory}/${binding.imported}.py`, `${directory}/${binding.imported}/__init__.py`].find(candidate => filesByPath.has(candidate));
|
|
211
|
+
return module ? { target: module, external: false } : null;
|
|
212
|
+
}
|
|
213
|
+
const rootName = receiver => receiver?.match(/^[$A-Za-z_][\w$]*/u)?.[0] || null;
|
|
214
|
+
|
|
215
|
+
function resolve(entry, call, from) {
|
|
216
|
+
const { language } = entry;
|
|
217
|
+
const name = call.name;
|
|
218
|
+
const receiver = call.receiver ?? null;
|
|
219
|
+
const unresolved = { resolution: 'unresolved', certainty: 'unresolved', method: 'none', targets: [] };
|
|
220
|
+
const found = (targets, method) => targets.length === 1
|
|
221
|
+
? { resolution: 'resolved', certainty: 'probable', method, targets }
|
|
222
|
+
: { resolution: 'ambiguous', certainty: 'probable', method, targets };
|
|
223
|
+
if (!name) return language === 'shell' ? null : unresolved;
|
|
224
|
+
if (receiver && PARENT_RECEIVERS.has(rootName(receiver))) return unresolved;
|
|
225
|
+
const self = receiver !== null && SELF_RECEIVERS[language]?.includes(receiver);
|
|
226
|
+
if (!receiver) {
|
|
227
|
+
const binding = entry.named.get(name);
|
|
228
|
+
if (binding?.target) {
|
|
229
|
+
const targets = binding.imported === 'default' ? generic.get(binding.target)?.defaults || [] : exportedFunctions(binding.target, binding.imported);
|
|
230
|
+
if (targets.length === 1) return found(targets, 'import-scope');
|
|
231
|
+
} else if (!binding) {
|
|
232
|
+
const owners = entry.wildcards.map(wildcard => exportedFunctions(wildcard.target, name)).filter(targets => targets.length);
|
|
233
|
+
if (owners.length === 1 && owners[0].length === 1) return found(owners[0], 'import-scope');
|
|
234
|
+
}
|
|
235
|
+
} else if (!self) {
|
|
236
|
+
const namespace = namespaceTarget(entry, receiver);
|
|
237
|
+
if (namespace?.target) {
|
|
238
|
+
const own = generic.get(namespace.target)?.byName.get(name) || [];
|
|
239
|
+
const targets = own.length ? own : exportedFunctions(namespace.target, name);
|
|
240
|
+
if (targets.length === 1) return found(targets, 'import-scope');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (self && from?.container) {
|
|
244
|
+
const targets = entry.members.get(`${from.container}\0${name}`) || [];
|
|
245
|
+
if (targets.length >= 1 && targets.length <= MAX_CALL_TARGETS) return found(targets, 'self');
|
|
246
|
+
}
|
|
247
|
+
// Provably external calls are omitted rather than shown as unresolved. A
|
|
248
|
+
// literal, imported-package or builtin receiver never reaches a project
|
|
249
|
+
// function, so name matching (rules 4 and 5) does not apply to it.
|
|
250
|
+
const root = rootName(receiver);
|
|
251
|
+
const boundExternal = value => value && (entry.named.get(value)?.external || entry.namespaces.get(value)?.external);
|
|
252
|
+
if (receiver && !self && (call.literal || boundExternal(root) || boundExternal(receiver) || BUILTIN_GLOBALS[language]?.has(root))) return null;
|
|
253
|
+
const same = entry.byName.get(name) || [];
|
|
254
|
+
if (same.length === 1) return found(same, 'same-file-name');
|
|
255
|
+
const candidates = [...entry.imports].flatMap(file => generic.get(file)?.byName.get(name) || []);
|
|
256
|
+
if (candidates.length >= 2 && candidates.length <= MAX_CALL_TARGETS) return found(candidates, 'receiver-name');
|
|
257
|
+
if (language === 'shell') return null;
|
|
258
|
+
if (!receiver && boundExternal(name)) return null;
|
|
259
|
+
if (!projectNames.has(name)) {
|
|
260
|
+
if (!receiver && BUILTIN_GLOBALS[language]?.has(name)) return null;
|
|
261
|
+
if (receiver && BUILTIN_MEMBERS[language]?.has(name)) return null;
|
|
262
|
+
if (language === 'python' && !receiver && /(?:Error|Exception|Warning|Exit|Interrupt)$/u.test(name)) return null;
|
|
263
|
+
}
|
|
264
|
+
return unresolved;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Per-file precedence: an adapter that declares any function in a file
|
|
268
|
+
// replaces the generic baseline there.
|
|
269
|
+
const functions = [...adapterFunctions.map(entry => ({ id: entry.id, file: entry.file, name: entry.name, kind: entry.kind, start: entry.start, end: entry.end,
|
|
270
|
+
...(entry.container ? { container: entry.container } : {}), ...(typeof entry.exported === 'boolean' ? { exported: entry.exported } : {}) }))];
|
|
271
|
+
const adapterById = new Map(functions.map(entry => [entry.id, entry]));
|
|
272
|
+
const adapterByFileName = new Map();
|
|
273
|
+
for (const entry of functions) {
|
|
274
|
+
const key = `${entry.file}\0${entry.name}`;
|
|
275
|
+
if (!adapterByFileName.has(key)) adapterByFileName.set(key, []);
|
|
276
|
+
adapterByFileName.get(key).push(entry);
|
|
277
|
+
}
|
|
278
|
+
for (const [filePath, entry] of generic) {
|
|
279
|
+
if (adapterFiles.has(filePath)) {
|
|
280
|
+
for (const fn of [entry.moduleRecord, ...entry.functions].filter(Boolean)) {
|
|
281
|
+
const adapter = adapterById.get(fn.id);
|
|
282
|
+
if (adapter && (adapter.name !== fn.name || adapter.kind !== fn.kind || comparePoints(adapter.end, fn.end))) diagnose({ code: 'FUNCTION_CONFLICT', path: filePath, message: `The ${adapterFiles.get(filePath)} inspector and the generic baseline disagree about ${fn.id}; the inspector is kept.` });
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (entry.moduleRecord) functions.push(entry.moduleRecord);
|
|
287
|
+
functions.push(...entry.functions.filter(fn => !fn.duplicate));
|
|
288
|
+
}
|
|
289
|
+
const rejoin = target => {
|
|
290
|
+
if (!adapterFiles.has(target.file)) return target.id;
|
|
291
|
+
if (adapterById.has(target.id)) return target.id;
|
|
292
|
+
const nearby = (adapterByFileName.get(`${target.file}\0${target.name}`) || [])
|
|
293
|
+
.map(entry => ({ entry, distance: Math.abs(entry.start.line - target.start.line) }))
|
|
294
|
+
.filter(entry => target.name && entry.distance <= REJOIN_LINES)
|
|
295
|
+
.sort((left, right) => left.distance - right.distance);
|
|
296
|
+
return nearby.length && (nearby.length === 1 || nearby[0].distance < nearby[1].distance) ? nearby[0].entry.id : null;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const calls = [];
|
|
300
|
+
const truncated = [];
|
|
301
|
+
for (const [filePath, entry] of generic) {
|
|
302
|
+
if (adapterFiles.has(filePath)) continue;
|
|
303
|
+
const fileCalls = [];
|
|
304
|
+
for (const call of entry.sites.calls) {
|
|
305
|
+
const from = call.from >= 0 ? entry.functions[call.from] : entry.moduleRecord;
|
|
306
|
+
if (!from || from.duplicate) continue;
|
|
307
|
+
const result = resolve(entry, call, from);
|
|
308
|
+
if (!result) continue;
|
|
309
|
+
let targets = result.targets.map(rejoin);
|
|
310
|
+
let outcome = result;
|
|
311
|
+
if (targets.some(target => target === null)) { outcome = { resolution: 'unresolved', certainty: 'unresolved', method: 'none' }; targets = []; }
|
|
312
|
+
targets = [...new Set(targets)];
|
|
313
|
+
if (outcome.resolution === 'ambiguous' && targets.length === 1) outcome = { ...outcome, resolution: 'resolved' };
|
|
314
|
+
fileCalls.push({ fromId: from.id, targets, certainty: outcome.certainty, method: outcome.method, resolution: outcome.resolution, callee: call.callee, site: { start: call.start, end: call.end } });
|
|
315
|
+
}
|
|
316
|
+
if (fileCalls.length > MAX_CALLS_PER_FILE) {
|
|
317
|
+
truncated.push(filePath);
|
|
318
|
+
diagnose({ code: 'CALLS_TRUNCATED', path: filePath, message: `Only the first ${MAX_CALLS_PER_FILE} calls of this file are recorded.` });
|
|
319
|
+
fileCalls.length = MAX_CALLS_PER_FILE;
|
|
320
|
+
}
|
|
321
|
+
calls.push(...fileCalls);
|
|
322
|
+
}
|
|
323
|
+
const functionIds = new Set(functions.map(entry => entry.id));
|
|
324
|
+
for (const call of observations?.calls || []) {
|
|
325
|
+
if (!functionIds.has(call.fromFunctionId) || !call.targets.every(target => functionIds.has(target))) continue;
|
|
326
|
+
calls.push({ fromId: call.fromFunctionId, targets: call.targets, certainty: call.certainty, method: call.method, resolution: call.resolution, callee: call.callee, site: { start: call.evidence[0].start, end: call.evidence[0].end } });
|
|
327
|
+
}
|
|
328
|
+
for (const entry of observations?.diagnostics || []) if (entry.code === 'FUNCTION_CONFLICT' || entry.code === 'CALLS_TRUNCATED') diagnose(entry);
|
|
329
|
+
|
|
330
|
+
// Each file's module code comes first, then its functions by position.
|
|
331
|
+
functions.sort((left, right) => filesByPath.get(left.file).index - filesByPath.get(right.file).index
|
|
332
|
+
|| Number(right.kind === 'module') - Number(left.kind === 'module') || comparePoints(left.start, right.start) || left.id.localeCompare(right.id));
|
|
333
|
+
const indexById = new Map(functions.map((entry, index) => [entry.id, index]));
|
|
334
|
+
calls.sort((left, right) => indexById.get(left.fromId) - indexById.get(right.fromId) || comparePoints(left.site.start, right.site.start) || comparePoints(left.site.end, right.site.end));
|
|
335
|
+
const bridges = [];
|
|
336
|
+
for (const connection of architecture.ui?.connections || []) {
|
|
337
|
+
if (connection.resolution !== 'resolved' || !indexById.has(connection.fromFunctionId) || !indexById.has(connection.controllerFunctionId)) continue;
|
|
338
|
+
bridges.push({ from: indexById.get(connection.fromFunctionId), to: indexById.get(connection.controllerFunctionId), kind: 'operation', operationKey: connection.key, certainty: connection.certainty === 'exact' ? 'exact' : 'probable' });
|
|
339
|
+
}
|
|
340
|
+
for (const [code, seen] of counted) if (seen > MAX_DIAGNOSTICS_PER_CODE) diagnostics.push({ code, message: `${seen - MAX_DIAGNOSTICS_PER_CODE} more ${code} diagnostics are not listed.` });
|
|
341
|
+
|
|
342
|
+
const coverage = { generic: 0, adapters: {} };
|
|
343
|
+
const document = {
|
|
344
|
+
schema: CALL_WEB_SCHEMA,
|
|
345
|
+
sourceIdentity,
|
|
346
|
+
files: files.map(file => {
|
|
347
|
+
const adapter = adapterFiles.get(file.path);
|
|
348
|
+
const value = adapter ? `adapter:${adapter}` : generic.has(file.path) ? 'generic' : 'none';
|
|
349
|
+
if (adapter) coverage.adapters[adapter] = (coverage.adapters[adapter] || 0) + 1;
|
|
350
|
+
else if (value === 'generic') coverage.generic++;
|
|
351
|
+
return { path: file.path, hash: file.hash, subsystemId: file.subsystemId, coverage: value };
|
|
352
|
+
}),
|
|
353
|
+
functions: functions.map(({ id, file, name, kind, container, exported: isExported, start, end }) => ({ id, file: filesByPath.get(file).index, name, kind, ...(container ? { container } : {}), ...(typeof isExported === 'boolean' ? { exported: isExported } : {}), start, end })),
|
|
354
|
+
calls: calls.map(call => ({ from: indexById.get(call.fromId), targets: call.targets.map(target => indexById.get(target)), certainty: call.certainty, method: call.method, callee: call.callee, site: call.site })),
|
|
355
|
+
bridges,
|
|
356
|
+
diagnostics,
|
|
357
|
+
};
|
|
358
|
+
const counts = { functions: document.functions.length, calls: document.calls.length, exact: 0, probable: 0, unresolved: 0 };
|
|
359
|
+
for (const call of document.calls) counts[call.certainty]++;
|
|
360
|
+
return { document, counts, coverage, truncated };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Validates a `genesis.call-web.v1` document; `files` (path/hash) pins it to a snapshot. */
|
|
364
|
+
export function validateCallWeb(document, { files } = {}) {
|
|
365
|
+
const check = (condition, message) => { if (!condition) throw new GenesisError('CALL_WEB_INVALID', message); };
|
|
366
|
+
const isPoint = value => value && Number.isSafeInteger(value.line) && value.line > 0 && Number.isSafeInteger(value.column) && value.column > 0;
|
|
367
|
+
const isRange = (start, end) => isPoint(start) && isPoint(end) && comparePoints(start, end) < 0;
|
|
368
|
+
check(document && typeof document === 'object' && document.schema === CALL_WEB_SCHEMA, `Expected ${CALL_WEB_SCHEMA}.`);
|
|
369
|
+
check(typeof document.sourceIdentity === 'string' && document.sourceIdentity.length > 0, 'A source identity is required.');
|
|
370
|
+
for (const name of ['files', 'functions', 'calls', 'bridges', 'diagnostics']) check(Array.isArray(document[name]), `${name} must be an array.`);
|
|
371
|
+
const known = files ? new Map(files.map(file => [file.path, file.hash])) : null;
|
|
372
|
+
const paths = new Set();
|
|
373
|
+
for (const file of document.files) {
|
|
374
|
+
check(typeof file?.path === 'string' && file.path.length > 0 && !paths.has(file.path) && typeof file.hash === 'string' && (file.subsystemId === null || typeof file.subsystemId === 'string'), 'Invalid or duplicate call-web file.');
|
|
375
|
+
check(file.coverage === 'generic' || file.coverage === 'none' || /^adapter:.+/u.test(file.coverage), `Invalid coverage for ${file.path}.`);
|
|
376
|
+
if (known) check(known.get(file.path) === file.hash, `${file.path} does not match the indexed source.`);
|
|
377
|
+
paths.add(file.path);
|
|
378
|
+
}
|
|
379
|
+
const ids = new Set();
|
|
380
|
+
for (const entry of document.functions) {
|
|
381
|
+
const file = document.files[entry?.file];
|
|
382
|
+
check(file && isRange(entry.start, entry.end) && entry.id === functionId(file.path, entry.start, entry.kind) && !ids.has(entry.id), 'Invalid or duplicate call-web function.');
|
|
383
|
+
check(FUNCTION_KINDS.includes(entry.kind) && typeof entry.name === 'string', `${entry.id} needs a known kind and a name.`);
|
|
384
|
+
ids.add(entry.id);
|
|
385
|
+
}
|
|
386
|
+
const fn = index => document.functions[index];
|
|
387
|
+
for (const call of document.calls) {
|
|
388
|
+
const from = fn(call?.from);
|
|
389
|
+
check(from && Array.isArray(call.targets) && call.targets.every(target => fn(target)), 'A call must reference known functions.');
|
|
390
|
+
check(CALL_CERTAINTIES.includes(call.certainty) && CALL_METHODS.includes(call.method) && typeof call.callee === 'string' && call.callee.length <= MAX_CALLEE_LENGTH, 'A call needs a known certainty, method and bounded callee.');
|
|
391
|
+
if (call.certainty === 'exact') check(call.targets.length === 1 && EXACT_CALL_METHODS.includes(call.method), 'An exact call needs one target from a resolver.');
|
|
392
|
+
if (call.certainty === 'probable') check(call.method !== 'none' && call.targets.length >= 1 && call.targets.length <= MAX_CALL_TARGETS, 'A probable call needs 1-16 targets.');
|
|
393
|
+
if (call.certainty === 'unresolved') check(call.targets.length === 0 && call.method === 'none', 'An unresolved call has no targets.');
|
|
394
|
+
check(isRange(call.site?.start, call.site?.end) && comparePoints(from.start, call.site.start) <= 0 && comparePoints(call.site.end, from.end) <= 0, 'A call site must lie inside its caller.');
|
|
395
|
+
}
|
|
396
|
+
for (const bridge of document.bridges) check(fn(bridge?.from) && fn(bridge.to) && bridge.kind === 'operation' && ['exact', 'probable'].includes(bridge.certainty) && bridge.operationKey && typeof bridge.operationKey === 'object', 'Invalid call-web bridge.');
|
|
397
|
+
check(document.diagnostics.every(entry => entry && typeof entry.code === 'string' && typeof entry.message === 'string'), 'diagnostics must contain code and message.');
|
|
398
|
+
return document;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Reads the current call web. `identity` is Machine City's `callWeb.identity`;
|
|
403
|
+
* without it the saved Machine City supplies it. Returns null when no call web
|
|
404
|
+
* matches the current Machine City.
|
|
405
|
+
*/
|
|
406
|
+
export async function readCallWeb({ projectRoot = process.cwd(), identity } = {}) {
|
|
407
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
408
|
+
let expected = identity;
|
|
409
|
+
if (expected === undefined) {
|
|
410
|
+
try { expected = JSON.parse(await readFile(path.join(root, '.genesis/machine-city.json'), 'utf8')).architecture?.callWeb?.identity ?? null; }
|
|
411
|
+
catch (error) { if (error instanceof SyntaxError || ['ENOENT', 'ENOTDIR'].includes(error.code)) expected = null; else throw error; }
|
|
412
|
+
}
|
|
413
|
+
if (!expected) return null;
|
|
414
|
+
let source;
|
|
415
|
+
try { source = await readFile(path.join(root, CALL_WEB_PATH)); }
|
|
416
|
+
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
|
|
417
|
+
if (sha256(source) !== expected) return null;
|
|
418
|
+
return validateCallWeb(JSON.parse(source.toString('utf8')));
|
|
419
|
+
}
|