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
|
@@ -9,7 +9,7 @@ const packageName = value => typeof value === 'string' && /^(?:@[a-zA-Z0-9._-]+\
|
|
|
9
9
|
|
|
10
10
|
// npm owns manifest and export semantics. Read direct declarations and their
|
|
11
11
|
// public entry files only; never walk dependency trees or enumerate internals.
|
|
12
|
-
export async function inspectNpmPackages({ projectRoot, discovery, files, indexedFiles = [], signal }) {
|
|
12
|
+
export async function inspectNpmPackages({ projectRoot, discovery, files, indexedFiles = [], cacheRoot = null, signal }) {
|
|
13
13
|
const known = new Map(files.map(file => [file.path, file]));
|
|
14
14
|
const packages = []; const inputs = []; const diagnostics = [];
|
|
15
15
|
const manifests = new Map(); const workspaceScopes = []; const byRoot = new Map();
|
|
@@ -145,7 +145,8 @@ export async function inspectNpmPackages({ projectRoot, discovery, files, indexe
|
|
|
145
145
|
}
|
|
146
146
|
const projectIndexes = new Map(indexedFiles.map(file => [file.path, file]));
|
|
147
147
|
const candidates = [...new Map(entries.filter(entry => projectIndexes.get(entry.file.path)?.hash !== entry.file.hash).map(entry => [entry.file.path, { path: entry.file.path, hash: entry.file.hash, mode: 0o100644, publicEntry: true }])).values()];
|
|
148
|
-
|
|
148
|
+
// Entry files share the project's extraction cache, so unchanged packages are not parsed again.
|
|
149
|
+
const result = candidates.length ? await extractCodeIndexes({ indexers: ['javascript'], files: candidates, projectRoot, signal, cacheRoot }) : { contributions: [], diagnostics: [], cacheLocations: [] };
|
|
149
150
|
diagnostics.push(...result.diagnostics);
|
|
150
151
|
const indexed = new Map([...projectIndexes, ...result.contributions.flatMap(contribution => contribution.files.map(file => [file.path, file]))]);
|
|
151
152
|
function publicSurface(filePath, visited = new Set()) {
|
|
@@ -184,7 +185,7 @@ export async function inspectNpmPackages({ projectRoot, discovery, files, indexe
|
|
|
184
185
|
point.hash = file.hash; point.coverage = 'partial'; record.symbolCoverage = 'partial';
|
|
185
186
|
point.symbols = publicSurface(file.path);
|
|
186
187
|
}
|
|
187
|
-
return { packages, inputs: [...new Map(inputs.map(input => [input.path, input])).values()], diagnostics };
|
|
188
|
+
return { packages, inputs: [...new Map(inputs.map(input => [input.path, input])).values()], diagnostics, cacheLocations: result.cacheLocations };
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
// Metadata and entry files live outside Git's inventory. Recheck those exact
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parse, registerDynamicLanguage } from '@ast-grep/napi';
|
|
2
2
|
import { astGrepCodeIndexers } from './ast-grep.js';
|
|
3
3
|
|
|
4
4
|
try {
|
|
@@ -7,9 +7,12 @@ try {
|
|
|
7
7
|
const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
8
8
|
if (Object.keys(input.registrations).length) registerDynamicLanguage(input.registrations);
|
|
9
9
|
const contributions = [];
|
|
10
|
+
const callSites = new Set(input.callSites || []);
|
|
10
11
|
for (const id of input.indexers) {
|
|
11
12
|
const result = await astGrepCodeIndexers.get(id).extract({
|
|
12
|
-
|
|
13
|
+
// Nothing else runs here while a file parses, so parsing stays on this
|
|
14
|
+
// thread instead of a round trip through the thread pool.
|
|
15
|
+
files: input.files, projectRoot: input.projectRoot, parse, parserErrors: input.errors, callSites,
|
|
13
16
|
});
|
|
14
17
|
contributions.push({ ...result, extractor: id });
|
|
15
18
|
}
|
|
@@ -74,6 +74,18 @@ const DIMENSIONS = {
|
|
|
74
74
|
guidance: 'Explain the practical reasoning behind decisions in a teaching-oriented way.',
|
|
75
75
|
},
|
|
76
76
|
},
|
|
77
|
+
longChecks: {
|
|
78
|
+
conservative: {
|
|
79
|
+
name: 'Conservative',
|
|
80
|
+
description: 'Saves the most tokens; results arrive only when each check ends.',
|
|
81
|
+
guidance: 'Run each long check as one unattended command, bounded by a time limit, that writes its complete output to a local file and exits with its status, so it ends with a single completion signal. Do not watch it while it runs: no progress monitors, log tails, repeated status checks or interim reports. Pass this on to any agent you delegate checks to.',
|
|
82
|
+
},
|
|
83
|
+
normal: {
|
|
84
|
+
name: 'Normal',
|
|
85
|
+
description: 'Reacts sooner, uses more tokens.',
|
|
86
|
+
guidance: 'You may follow a long check\'s progress while it runs and stop a clearly broken run early. Still keep its full output in files and read only what you need.',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
77
89
|
};
|
|
78
90
|
|
|
79
91
|
const SECTION_FIELDS = Object.freeze({
|
|
@@ -81,8 +93,13 @@ const SECTION_FIELDS = Object.freeze({
|
|
|
81
93
|
'Response length': 'responseLength',
|
|
82
94
|
'Assumed experience': 'experience',
|
|
83
95
|
'Explanation style': 'explanationStyle',
|
|
96
|
+
'Long checks': 'longChecks',
|
|
84
97
|
});
|
|
85
98
|
|
|
99
|
+
// A missing optional section is an unmade choice, not a default.
|
|
100
|
+
const OPTIONAL_SECTIONS = new Set(['Long checks']);
|
|
101
|
+
const LONG_CHECKS_UNTIL_CHOSEN = 'conservative';
|
|
102
|
+
|
|
86
103
|
export const DEFAULT_COLLABORATION = Object.freeze({
|
|
87
104
|
tone: 'encouraging',
|
|
88
105
|
responseLength: 'concise',
|
|
@@ -196,6 +213,15 @@ function collaborationCatalog() {
|
|
|
196
213
|
]));
|
|
197
214
|
}
|
|
198
215
|
|
|
216
|
+
function unsetLongChecksGuidance(value) {
|
|
217
|
+
const choices = DIMENSIONS.longChecks;
|
|
218
|
+
return [
|
|
219
|
+
'The project has not chosen how agents handle long checks such as test suites and builds. Ask the person once, early, before running the first long check, which option they want:',
|
|
220
|
+
...Object.entries(choices).map(([id, choice]) => `- \`${id}\`: ${choice.description}`),
|
|
221
|
+
`Record the answer with \`genesis collaboration set ${value.tone} ${value.responseLength} ${value.experience} ${value.explanationStyle} <${Object.keys(choices).join('|')}>\`. Until the person answers, use \`${LONG_CHECKS_UNTIL_CHOSEN}\`: ${choices[LONG_CHECKS_UNTIL_CHOSEN].guidance}`,
|
|
222
|
+
].join('\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
199
225
|
function collaborationGuidance(value) {
|
|
200
226
|
return [
|
|
201
227
|
'Apply the configured tone, response length, assumed experience, and explanation style to every user-facing message, including progress updates and final responses.',
|
|
@@ -217,6 +243,10 @@ function collaborationGuidance(value) {
|
|
|
217
243
|
'## Explanation style',
|
|
218
244
|
'',
|
|
219
245
|
DIMENSIONS.explanationStyle[value.explanationStyle].guidance,
|
|
246
|
+
'',
|
|
247
|
+
'## Long checks',
|
|
248
|
+
'',
|
|
249
|
+
value.longChecks ? DIMENSIONS.longChecks[value.longChecks].guidance : unsetLongChecksGuidance(value),
|
|
220
250
|
...(value.requirements ? [
|
|
221
251
|
'',
|
|
222
252
|
'## Project requirements',
|
|
@@ -231,17 +261,17 @@ export function parseCollaborationSource(source) {
|
|
|
231
261
|
const parsed = sections(source);
|
|
232
262
|
const expected = new Set([...Object.keys(SECTION_FIELDS), 'Project requirements']);
|
|
233
263
|
const unknown = [...parsed.sections.keys()].filter((name) => !expected.has(name));
|
|
234
|
-
const missing = [...expected].filter((name) => !parsed.sections.has(name));
|
|
264
|
+
const missing = [...expected].filter((name) => !parsed.sections.has(name) && !OPTIONAL_SECTIONS.has(name));
|
|
235
265
|
if (unknown.length > 0 || missing.length > 0) {
|
|
236
266
|
throw new GenesisError(
|
|
237
267
|
'COLLABORATION_INVALID',
|
|
238
|
-
`${COLLABORATION_PATH} needs only Tone, Response length, Assumed experience, Explanation style, and Project requirements sections.`,
|
|
268
|
+
`${COLLABORATION_PATH} needs only Tone, Response length, Assumed experience, Explanation style, optional Long checks, and Project requirements sections.`,
|
|
239
269
|
{ path: COLLABORATION_PATH },
|
|
240
270
|
);
|
|
241
271
|
}
|
|
242
272
|
const values = Object.fromEntries(Object.entries(SECTION_FIELDS).map(([sectionName, field]) => [
|
|
243
273
|
field,
|
|
244
|
-
dimensionValue(parsed.sections, sectionName, field),
|
|
274
|
+
parsed.sections.has(sectionName) ? dimensionValue(parsed.sections, sectionName, field) : null,
|
|
245
275
|
]));
|
|
246
276
|
const requirementsSource = sectionText(parsed.sections, 'Project requirements');
|
|
247
277
|
if (!requirementsSource) {
|
|
@@ -296,6 +326,7 @@ function renderCollaboration(value) {
|
|
|
296
326
|
'',
|
|
297
327
|
`- \`${value.explanationStyle}\``,
|
|
298
328
|
'',
|
|
329
|
+
...(value.longChecks === null ? [] : ['## Long checks', '', `- \`${value.longChecks}\``, '']),
|
|
299
330
|
'## Project requirements',
|
|
300
331
|
'',
|
|
301
332
|
value.requirements || '- Nothing.',
|
|
@@ -340,6 +371,7 @@ export async function inspectProjectCollaboration({ projectRoot } = {}) {
|
|
|
340
371
|
export async function setProjectCollaboration({
|
|
341
372
|
experience,
|
|
342
373
|
explanationStyle,
|
|
374
|
+
longChecks,
|
|
343
375
|
projectRoot,
|
|
344
376
|
requirements,
|
|
345
377
|
responseLength,
|
|
@@ -352,6 +384,7 @@ export async function setProjectCollaboration({
|
|
|
352
384
|
responseLength: responseLength ?? current.responseLength,
|
|
353
385
|
experience: experience ?? current.experience,
|
|
354
386
|
explanationStyle: explanationStyle ?? current.explanationStyle,
|
|
387
|
+
longChecks: longChecks ?? current.longChecks,
|
|
355
388
|
requirements: requirements ?? current.requirements,
|
|
356
389
|
}));
|
|
357
390
|
const rendered = renderCollaboration(next);
|
|
@@ -367,6 +400,7 @@ export async function setProjectCollaboration({
|
|
|
367
400
|
responseLength: next.responseLength,
|
|
368
401
|
experience: next.experience,
|
|
369
402
|
explanationStyle: next.explanationStyle,
|
|
403
|
+
longChecks: next.longChecks,
|
|
370
404
|
requirements: next.requirements,
|
|
371
405
|
choices: collaborationCatalog(),
|
|
372
406
|
guidance: collaborationGuidance(next),
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { ENTRY_POINT_KINDS } from './architecture-observations.js';
|
|
5
|
+
import { sha256 } from './utils.js';
|
|
6
|
+
|
|
7
|
+
export const MAX_ENTRY_POINTS = 20_000;
|
|
8
|
+
export const MAX_TEST_ENTRY_POINTS = 5_000;
|
|
9
|
+
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
10
|
+
const TEST_FILE = [
|
|
11
|
+
/(?:^|\/)[^/]+\.(?:test|spec)\.[^/]+$/u, /(?:^|\/)test_[^/]+\.py$/u, /_test\.py$/u, /_test\.go$/u, /(?:^|\/)src\/test\//u,
|
|
12
|
+
/Test\.java$/u, /Test\.kt$/u, /Tests\.cs$/u, /(?:^|\/)spec\/(?:.+\/)?[^/]+_spec\.rb$/u, /(?:^|\/)tests\/(?:.+\/)?[^/]+\.rs$/u, /(?:^|\/)tests\/(?:.+\/)?[^/]+Test\.php$/u,
|
|
13
|
+
];
|
|
14
|
+
const KIND_ORDER = new Map(ENTRY_POINT_KINDS.map((kind, index) => [kind, index]));
|
|
15
|
+
const isTestFile = filePath => TEST_FILE.some(pattern => pattern.test(filePath));
|
|
16
|
+
|
|
17
|
+
const lineRange = (source, needle) => {
|
|
18
|
+
const lines = source.split('\n');
|
|
19
|
+
const index = needle ? lines.findIndex(line => line.includes(needle)) : -1;
|
|
20
|
+
const line = index < 0 ? 1 : index + 1;
|
|
21
|
+
const length = (lines[line - 1] || '').replace(/\r$/u, '').length;
|
|
22
|
+
return length ? { start: { line, column: 1 }, end: { line, column: length + 1 } } : { start: { line, column: 1 }, end: { line: line + 1, column: 1 } };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Minimal TOML: `[section]` / `[[section]]` headers with `key = "value"` lines.
|
|
26
|
+
function tomlSections(source) {
|
|
27
|
+
const sections = [];
|
|
28
|
+
let current = { name: '', values: {} };
|
|
29
|
+
sections.push(current);
|
|
30
|
+
for (const raw of source.split('\n')) {
|
|
31
|
+
const line = raw.replace(/#.*$/u, '').trim();
|
|
32
|
+
const header = line.match(/^\[\[?\s*([^\]]+?)\s*\]\]?$/u);
|
|
33
|
+
if (header) { current = { name: header[1], values: {} }; sections.push(current); continue; }
|
|
34
|
+
const pair = line.match(/^"?([^"=\s]+)"?\s*=\s*["']([^"']*)["']/u);
|
|
35
|
+
if (pair) current.values[pair[1]] = pair[2];
|
|
36
|
+
}
|
|
37
|
+
return sections;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Discovers language-level entry points from manifests, conventions, Program
|
|
42
|
+
* operations and adapter observations. Function identities are attached only
|
|
43
|
+
* when a call web exists (`attachEntryFunctions`).
|
|
44
|
+
*/
|
|
45
|
+
export async function discoverEntryPoints({ projectRoot, architecture, functions = [], program = null }) {
|
|
46
|
+
const files = new Map(architecture.files.map(file => [file.path, file]));
|
|
47
|
+
const sources = new Map();
|
|
48
|
+
async function read(filePath) {
|
|
49
|
+
if (sources.has(filePath)) return sources.get(filePath);
|
|
50
|
+
let source = null;
|
|
51
|
+
const file = files.get(filePath);
|
|
52
|
+
if (file) {
|
|
53
|
+
try {
|
|
54
|
+
const bytes = await readFile(path.join(projectRoot, filePath));
|
|
55
|
+
if (bytes.length <= MAX_MANIFEST_BYTES && sha256(bytes) === file.hash) source = bytes.toString('utf8');
|
|
56
|
+
} catch (error) { if (!['ENOENT', 'ENOTDIR', 'EISDIR'].includes(error.code)) throw error; }
|
|
57
|
+
}
|
|
58
|
+
sources.set(filePath, source);
|
|
59
|
+
return source;
|
|
60
|
+
}
|
|
61
|
+
const entries = new Map();
|
|
62
|
+
const evidence = (filePath, range) => [{ path: filePath, hash: files.get(filePath).hash, ...range }];
|
|
63
|
+
function add(record, target = null) {
|
|
64
|
+
if (!files.has(record.file) || entries.has(record.id)) return;
|
|
65
|
+
entries.set(record.id, { resolution: 'resolved', functionIds: [], pageId: null, componentId: null, operationKey: null, actionId: null, ...record, target });
|
|
66
|
+
}
|
|
67
|
+
const join = (directory, value) => path.posix.normalize(path.posix.join(directory, String(value).replace(/^\.\//u, '')));
|
|
68
|
+
const byFile = new Map();
|
|
69
|
+
for (const entry of functions) {
|
|
70
|
+
if (!byFile.has(entry.path)) byFile.set(entry.path, []);
|
|
71
|
+
byFile.get(entry.path).push(entry);
|
|
72
|
+
}
|
|
73
|
+
const paths = [...files.keys()];
|
|
74
|
+
const manifests = name => paths.filter(filePath => path.posix.basename(filePath) === name);
|
|
75
|
+
let libraryExports = 0;
|
|
76
|
+
|
|
77
|
+
for (const manifest of manifests('package.json')) {
|
|
78
|
+
const source = await read(manifest);
|
|
79
|
+
let document;
|
|
80
|
+
try { document = JSON.parse(source); } catch { continue; }
|
|
81
|
+
const bin = typeof document?.bin === 'string' ? { [String(document.name || '').split('/').at(-1) || path.posix.basename(document.bin)]: document.bin } : document?.bin;
|
|
82
|
+
for (const [name, value] of Object.entries(bin && typeof bin === 'object' ? bin : {})) {
|
|
83
|
+
if (typeof value !== 'string') continue;
|
|
84
|
+
const file = join(path.posix.dirname(manifest), value);
|
|
85
|
+
add({ id: `entry:cli-command:${manifest}#bin/${name}`, kind: 'cli-command', title: name, file, evidence: evidence(manifest, lineRange(source, `"${name}"`)) }, { module: file });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const manifest of manifests('pyproject.toml')) {
|
|
89
|
+
const source = await read(manifest);
|
|
90
|
+
if (source === null) continue;
|
|
91
|
+
const directory = path.posix.dirname(manifest);
|
|
92
|
+
const moduleFile = module => ['', 'src/'].flatMap(prefix => [`${prefix}${module.replaceAll('.', '/')}.py`, `${prefix}${module.replaceAll('.', '/')}/__init__.py`])
|
|
93
|
+
.map(candidate => join(directory, candidate)).find(candidate => files.has(candidate));
|
|
94
|
+
for (const section of tomlSections(source)) {
|
|
95
|
+
if (section.name === 'project.scripts') {
|
|
96
|
+
for (const [name, value] of Object.entries(section.values)) {
|
|
97
|
+
const [module, attribute] = value.split(':');
|
|
98
|
+
const file = moduleFile(module.trim());
|
|
99
|
+
if (file) add({ id: `entry:cli-command:${manifest}#scripts/${name}`, kind: 'cli-command', title: name, file, evidence: evidence(manifest, lineRange(source, name)) }, { name: attribute?.trim().split('.')[0], file });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (section.name === 'project' && section.values.name) {
|
|
103
|
+
const file = moduleFile(section.values.name.toLowerCase().replace(/[-.]+/gu, '_'));
|
|
104
|
+
if (file?.endsWith('__init__.py')) {
|
|
105
|
+
for (const entry of (byFile.get(file) || []).filter(entry => entry.visibility === 'public' && !entry.container)) {
|
|
106
|
+
libraryExports++;
|
|
107
|
+
add({ id: `entry:library-export:${file}#${entry.name}`, kind: 'library-export', title: entry.name, file, evidence: evidence(manifest, lineRange(source, 'name')) }, { name: entry.name, file });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const manifest of manifests('Cargo.toml')) {
|
|
114
|
+
const source = await read(manifest);
|
|
115
|
+
if (source === null) continue;
|
|
116
|
+
const directory = path.posix.dirname(manifest);
|
|
117
|
+
const sections = tomlSections(source);
|
|
118
|
+
for (const section of sections.filter(entry => entry.name === 'bin' && entry.values.name)) {
|
|
119
|
+
const file = join(directory, section.values.path || `src/bin/${section.values.name}.rs`);
|
|
120
|
+
add({ id: `entry:cli-command:${manifest}#bin/${section.values.name}`, kind: 'cli-command', title: section.values.name, file, evidence: evidence(manifest, lineRange(source, section.values.name)) }, { name: 'main', file });
|
|
121
|
+
}
|
|
122
|
+
const main = join(directory, 'src/main.rs');
|
|
123
|
+
const crate = sections.find(entry => entry.name === 'package')?.values.name || path.posix.basename(directory === '.' ? projectRoot : directory);
|
|
124
|
+
add({ id: `entry:cli-command:${manifest}#main`, kind: 'cli-command', title: crate, file: main, evidence: evidence(manifest, lineRange(source, '[package]')) }, { name: 'main', file: main });
|
|
125
|
+
const library = join(directory, 'src/lib.rs');
|
|
126
|
+
for (const entry of (byFile.get(library) || []).filter(entry => entry.visibility === 'public' && !entry.container)) {
|
|
127
|
+
libraryExports++;
|
|
128
|
+
add({ id: `entry:library-export:${library}#${entry.name}`, kind: 'library-export', title: entry.name, file: library, evidence: evidence(manifest, lineRange(source, '[package]')) }, { name: entry.name, file: library });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const manifest of paths.filter(filePath => filePath.endsWith('.gemspec'))) {
|
|
132
|
+
const source = await read(manifest);
|
|
133
|
+
if (source === null) continue;
|
|
134
|
+
const list = source.match(/executables\s*=\s*(?:\[([^\]]*)\]|%w[[(]([^\])]*)[\])])/u);
|
|
135
|
+
const names = list ? (list[1] !== undefined ? [...list[1].matchAll(/['"]([^'"]+)['"]/gu)].map(match => match[1]) : list[2].trim().split(/\s+/u)) : [];
|
|
136
|
+
const bindir = source.match(/bindir\s*=\s*['"]([^'"]+)['"]/u)?.[1] || 'bin';
|
|
137
|
+
for (const name of names.filter(Boolean)) {
|
|
138
|
+
const file = join(path.posix.dirname(manifest), `${bindir}/${name}`);
|
|
139
|
+
add({ id: `entry:cli-command:${manifest}#executables/${name}`, kind: 'cli-command', title: name, file, evidence: evidence(manifest, lineRange(source, 'executables')) }, { module: file });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const manifest of manifests('composer.json')) {
|
|
143
|
+
const source = await read(manifest);
|
|
144
|
+
let document;
|
|
145
|
+
try { document = JSON.parse(source); } catch { continue; }
|
|
146
|
+
for (const value of [document?.bin].flat().filter(entry => typeof entry === 'string')) {
|
|
147
|
+
const file = join(path.posix.dirname(manifest), value);
|
|
148
|
+
add({ id: `entry:cli-command:${manifest}#bin/${value}`, kind: 'cli-command', title: path.posix.basename(value), file, evidence: evidence(manifest, lineRange(source, value)) }, { module: file });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Language entry functions: Go `package main`, Java/Kotlin, C#, C++ `main`.
|
|
152
|
+
const goPackages = new Map();
|
|
153
|
+
for (const [filePath, file] of files) {
|
|
154
|
+
if (file.language !== 'go') continue;
|
|
155
|
+
const source = await read(filePath);
|
|
156
|
+
goPackages.set(filePath, source?.match(/^\s*package\s+(\w+)/mu)?.[1] || null);
|
|
157
|
+
}
|
|
158
|
+
for (const entry of functions) {
|
|
159
|
+
const file = files.get(entry.path);
|
|
160
|
+
const main = (entry.language === 'go' && entry.name === 'main' && !entry.container && goPackages.get(entry.path) === 'main')
|
|
161
|
+
|| (entry.language === 'java' && entry.name === 'main' && entry.static)
|
|
162
|
+
|| (entry.language === 'kotlin' && entry.name === 'main' && !entry.container)
|
|
163
|
+
|| (entry.language === 'csharp' && entry.name === 'Main')
|
|
164
|
+
|| (entry.language === 'cpp' && entry.name === 'main' && !entry.container);
|
|
165
|
+
if (main && file) add({ id: `entry:cli-command:${entry.path}#${entry.name}`, kind: 'cli-command', title: entry.container || path.posix.basename(entry.path).replace(/\.[^.]+$/u, ''), file: entry.path, evidence: evidence(entry.path, entry.start && entry.end ? { start: entry.start, end: entry.end } : lineRange('', null)) }, { name: entry.name, file: entry.path });
|
|
166
|
+
if (entry.language === 'go' && entry.visibility === 'public' && !entry.container && !isTestFile(entry.path) && goPackages.get(entry.path) && goPackages.get(entry.path) !== 'main') {
|
|
167
|
+
libraryExports++;
|
|
168
|
+
add({ id: `entry:library-export:${entry.path}#${entry.name}`, kind: 'library-export', title: entry.name, file: entry.path, evidence: evidence(entry.path, entry.start && entry.end ? { start: entry.start, end: entry.end } : lineRange('', null)) }, { name: entry.name, file: entry.path });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const filePath of paths.filter(value => path.posix.basename(value) === 'Program.cs' && !(byFile.get(value) || []).some(entry => entry.name === 'Main'))) {
|
|
172
|
+
add({ id: `entry:cli-command:${filePath}#top-level`, kind: 'cli-command', title: path.posix.basename(path.posix.dirname(filePath)) || 'Program', file: filePath, evidence: evidence(filePath, lineRange(await read(filePath) || '', null)) }, { module: filePath });
|
|
173
|
+
}
|
|
174
|
+
const commandFiles = new Set([...entries.values()].filter(entry => entry.kind === 'cli-command').map(entry => entry.file));
|
|
175
|
+
for (const [filePath, file] of files) {
|
|
176
|
+
if (file.mode !== 0o100755 || commandFiles.has(filePath)) continue;
|
|
177
|
+
const source = await read(filePath);
|
|
178
|
+
if (!source?.startsWith('#!')) continue;
|
|
179
|
+
add({ id: `entry:cli-command:${filePath}`, kind: 'cli-command', title: path.posix.basename(filePath), file: filePath, evidence: evidence(filePath, lineRange(source, '#!')) }, { module: filePath });
|
|
180
|
+
commandFiles.add(filePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const entry of architecture.packages || []) {
|
|
184
|
+
if (entry.opaque) continue;
|
|
185
|
+
for (const exported of entry.entryPoints || []) {
|
|
186
|
+
const specifier = exported.importPath === '.' ? entry.name : `${entry.name}${exported.importPath.slice(1)}`;
|
|
187
|
+
for (const symbol of exported.symbols || []) {
|
|
188
|
+
if (symbol.kind !== 'function' || !files.has(symbol.sourceFile)) continue;
|
|
189
|
+
libraryExports++;
|
|
190
|
+
add({ id: `entry:library-export:${symbol.sourceFile}#${symbol.sourceName || symbol.name}`, kind: 'library-export', title: `${symbol.name} (${specifier})`, file: symbol.sourceFile, evidence: evidence(symbol.sourceFile, { start: symbol.start, end: symbol.end }) }, { name: symbol.sourceName || symbol.name, file: symbol.sourceFile, within: { start: symbol.start, end: symbol.end } });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!libraryExports) {
|
|
195
|
+
// Without a declared package API, public functions outside commands and tests stand in for it.
|
|
196
|
+
for (const entry of functions.filter(value => value.visibility === 'public' && !value.container && value.role !== 'test' && !isTestFile(value.path) && !commandFiles.has(value.path) && value.start && value.end)) {
|
|
197
|
+
add({ id: `entry:library-export:${entry.path}#${entry.name}`, kind: 'library-export', title: entry.name, file: entry.path, evidence: evidence(entry.path, { start: entry.start, end: entry.end }) }, { name: entry.name, file: entry.path, within: { start: entry.start, end: entry.end } });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const operation of program?.buildings || []) {
|
|
201
|
+
const cited = operation.sources.filter(source => files.has(source));
|
|
202
|
+
if (!cited.length) continue;
|
|
203
|
+
const file = cited.includes(operation.mainSource) ? operation.mainSource : cited[0];
|
|
204
|
+
const names = [...new Set([...String(operation.publicContract || '').matchAll(/`(?:[A-Za-z_$][\w$]*[.:]+)*([A-Za-z_$][\w$]*)\s*\(/gu)].map(match => match[1]))];
|
|
205
|
+
add({ id: `entry:program-operation:${operation.path}`, kind: 'program-operation', title: operation.title, file, evidence: evidence(files.has(operation.path) ? operation.path : file, lineRange('', null)) }, { files: cited, names, module: file });
|
|
206
|
+
}
|
|
207
|
+
for (const [filePath, file] of files) {
|
|
208
|
+
if (file.language !== 'unknown' && isTestFile(filePath)) add({ id: `entry:test:${filePath}`, kind: 'test', title: path.posix.basename(filePath), file: filePath, evidence: evidence(filePath, lineRange('', null)) }, { all: filePath });
|
|
209
|
+
}
|
|
210
|
+
const ui = architecture.ui;
|
|
211
|
+
// A page entry is named by its first registration (for a web page, its URL
|
|
212
|
+
// path), which says more than a title taken from a file such as `[slug].vue`.
|
|
213
|
+
const registered = new Map();
|
|
214
|
+
for (const registration of ui?.registrations || []) if (!registered.has(registration.pageId)) registered.set(registration.pageId, registration.identity);
|
|
215
|
+
for (const page of ui?.pages || []) add({ id: `entry:page:${page.id}`, kind: 'page', title: registered.get(page.id) || page.title, file: page.file, pageId: page.id, evidence: page.evidence });
|
|
216
|
+
for (const component of ui?.components || []) if (component.file) add({ id: `entry:component:${component.id}`, kind: 'component', title: component.title, file: component.file, componentId: component.id, evidence: component.evidence });
|
|
217
|
+
for (const operation of ui?.entryOperations || []) {
|
|
218
|
+
if (operation.resolution !== 'resolved' || !operation.controllerFile) continue;
|
|
219
|
+
add({ id: `entry:route:${operation.id}`, kind: 'route', title: [operation.key.method, operation.key.operation].filter(Boolean).join(' '), file: operation.controllerFile, operationKey: operation.key, actionId: operation.actionId ?? null, evidence: operation.evidence }, { ids: operation.controllerFunctionId ? [operation.controllerFunctionId] : [] });
|
|
220
|
+
}
|
|
221
|
+
for (const record of ui?.entryPoints || []) {
|
|
222
|
+
add({ id: record.id.startsWith('entry:') ? record.id : `entry:${record.kind}:${record.id}`, kind: record.kind, title: record.title, file: record.file, pageId: record.pageId ?? null, componentId: record.componentId ?? null, operationKey: record.operationKey ?? null, actionId: record.actionId ?? null, evidence: record.evidence }, { ids: record.functionIds });
|
|
223
|
+
}
|
|
224
|
+
return capEntryPoints([...entries.values()]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Applies the 20,000 total / 5,000 test caps in a stable order. */
|
|
228
|
+
export function capEntryPoints(entries, { total = MAX_ENTRY_POINTS, tests = MAX_TEST_ENTRY_POINTS } = {}) {
|
|
229
|
+
const sorted = [...entries].sort((left, right) => KIND_ORDER.get(left.kind) - KIND_ORDER.get(right.kind) || left.file.localeCompare(right.file) || left.id.localeCompare(right.id));
|
|
230
|
+
let testCount = 0;
|
|
231
|
+
const kept = sorted.filter(entry => entry.kind !== 'test' || ++testCount <= tests).slice(0, total);
|
|
232
|
+
const truncated = kept.length < sorted.length;
|
|
233
|
+
return {
|
|
234
|
+
entries: kept,
|
|
235
|
+
capability: truncated ? 'partial' : 'complete',
|
|
236
|
+
diagnostics: truncated ? [{ code: 'ENTRY_POINTS_TRUNCATED', message: `Only ${kept.length} of ${sorted.length} entry points are listed (at most ${total}, of which ${tests} tests).` }] : [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Fills `functionIds` from a call web; without one every list stays empty. */
|
|
241
|
+
export function attachEntryFunctions(entries, callWeb) {
|
|
242
|
+
const byFile = new Map();
|
|
243
|
+
if (callWeb) {
|
|
244
|
+
for (const entry of callWeb.functions) {
|
|
245
|
+
const filePath = callWeb.files[entry.file].path;
|
|
246
|
+
if (!byFile.has(filePath)) byFile.set(filePath, []);
|
|
247
|
+
byFile.get(filePath).push(entry);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const known = new Set(callWeb ? callWeb.functions.map(entry => entry.id) : []);
|
|
251
|
+
const inside = (entry, range) => !range || ((entry.start.line > range.start.line || (entry.start.line === range.start.line && entry.start.column >= range.start.column))
|
|
252
|
+
&& (entry.start.line < range.end.line || (entry.start.line === range.end.line && entry.start.column < range.end.column)));
|
|
253
|
+
const module = filePath => (byFile.get(filePath) || []).filter(entry => entry.kind === 'module').map(entry => entry.id);
|
|
254
|
+
return entries.map(({ target, ...entry }) => {
|
|
255
|
+
let ids = [];
|
|
256
|
+
if (callWeb && target) {
|
|
257
|
+
if (target.ids) ids = target.ids.filter(id => known.has(id));
|
|
258
|
+
else if (target.all) ids = (byFile.get(target.all) || []).map(fn => fn.id);
|
|
259
|
+
else if (target.files) {
|
|
260
|
+
ids = target.files.flatMap(filePath => (byFile.get(filePath) || []).filter(fn => fn.name && target.names.includes(fn.name)).map(fn => fn.id));
|
|
261
|
+
if (!ids.length) ids = module(target.module);
|
|
262
|
+
} else if (target.name) {
|
|
263
|
+
const candidates = (byFile.get(target.file) || []).filter(fn => fn.name === target.name && inside(fn, target.within));
|
|
264
|
+
ids = candidates.length ? [candidates[0].id] : module(target.file);
|
|
265
|
+
} else if (target.module) ids = module(target.module);
|
|
266
|
+
}
|
|
267
|
+
return { ...entry, functionIds: [...new Set(ids)] };
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { access, readFile, readdir, rm, rmdir, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { runGitText } from './process.js';
|
|
6
|
+
import { writeFileAtomic } from './utils.js';
|
|
7
|
+
|
|
8
|
+
export const EXTRACTION_CACHE_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const STORE_BATCH = 64;
|
|
10
|
+
|
|
11
|
+
// Git never lists its own directory, so worktrees of one repository share
|
|
12
|
+
// these entries without an ignore line. Each kind of extraction has its own root.
|
|
13
|
+
export async function extractionCacheRoot(projectRoot, kind) {
|
|
14
|
+
const common = await runGitText(projectRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
15
|
+
return path.join(common, 'genesis-cache', kind);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One entry per extractor version, language and parsed content. */
|
|
19
|
+
export function extractionCachePath(root, { version, language, variant, hash }) {
|
|
20
|
+
const key = createHash('sha256').update(`${variant}\n${hash}`).digest('hex');
|
|
21
|
+
return path.join(root, String(version), language, `${key}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Reads cached extractions; unreadable or missing entries are extracted again. */
|
|
25
|
+
export async function loadExtractions(root, entries) {
|
|
26
|
+
const values = new Map();
|
|
27
|
+
const misses = [];
|
|
28
|
+
for (let index = 0; index < entries.length; index += STORE_BATCH) {
|
|
29
|
+
await Promise.all(entries.slice(index, index + STORE_BATCH).map(async entry => {
|
|
30
|
+
try { values.set(entry.path, JSON.parse(await readFile(entry.location, 'utf8'))); }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!(error instanceof SyntaxError) && !['ENOENT', 'ENOTDIR'].includes(error.code)) throw error;
|
|
33
|
+
misses.push(entry);
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
return { values, misses: misses.sort((left, right) => left.path.localeCompare(right.path)) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Entries without a cache file. Only presence is checked; nothing is read. */
|
|
41
|
+
export async function absentExtractions(entries) {
|
|
42
|
+
const absent = [];
|
|
43
|
+
for (let index = 0; index < entries.length; index += STORE_BATCH) {
|
|
44
|
+
await Promise.all(entries.slice(index, index + STORE_BATCH).map(async entry => {
|
|
45
|
+
try { await access(entry.location); }
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error.code)) throw error;
|
|
48
|
+
absent.push(entry);
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
return absent.sort((left, right) => left.path.localeCompare(right.path));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function storeExtractions(entries) {
|
|
56
|
+
for (let index = 0; index < entries.length; index += STORE_BATCH) {
|
|
57
|
+
await Promise.all(entries.slice(index, index + STORE_BATCH).map(({ location, value }) => writeFileAtomic(location, JSON.stringify(value))));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Deletes entries this snapshot does not use once they are older than the retention period. */
|
|
62
|
+
export async function pruneExtractions(root, referenced, { now = Date.now(), retentionMs = EXTRACTION_CACHE_RETENTION_MS } = {}) {
|
|
63
|
+
const keep = new Set(referenced);
|
|
64
|
+
let removed = 0;
|
|
65
|
+
async function visit(directory, depth) {
|
|
66
|
+
let names;
|
|
67
|
+
try { names = await readdir(directory); }
|
|
68
|
+
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return; throw error; }
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const location = path.join(directory, name);
|
|
71
|
+
if (depth < 2) { await visit(location, depth + 1); continue; }
|
|
72
|
+
if (keep.has(location)) continue;
|
|
73
|
+
try {
|
|
74
|
+
if (now - (await stat(location)).mtimeMs > retentionMs) { await rm(location, { force: true }); removed++; }
|
|
75
|
+
} catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error.code)) throw error; }
|
|
76
|
+
}
|
|
77
|
+
if (depth > 0) await rmdir(directory).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
await visit(root, 0);
|
|
80
|
+
return { removed };
|
|
81
|
+
}
|
|
@@ -15,11 +15,15 @@ async function visiblePaths(projectRoot) {
|
|
|
15
15
|
.sort();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const STATE_BATCH = 32;
|
|
19
|
+
|
|
18
20
|
/** Hashable states for ordinary Git-visible files, excluding ignored output. */
|
|
19
21
|
export async function gitVisibleFileStates(projectRoot, { includePath = () => true } = {}) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const files = (await visiblePaths(projectRoot)).filter(includePath);
|
|
23
|
+
// Entries keep path order while a bounded batch of files is read at once.
|
|
24
|
+
const states = new Map(files.map(file => [file, null]));
|
|
25
|
+
for (let index = 0; index < files.length; index += STATE_BATCH) {
|
|
26
|
+
await Promise.all(files.slice(index, index + STATE_BATCH).map(async file => states.set(file, await pathState(path.join(projectRoot, file)))));
|
|
23
27
|
}
|
|
24
28
|
return states;
|
|
25
29
|
}
|
|
@@ -24,6 +24,7 @@ export const GENESIS_BOOTSTRAP_PATHS = Object.freeze([
|
|
|
24
24
|
'genesis/engineering.md',
|
|
25
25
|
'.genesis/machine-city.json',
|
|
26
26
|
'.genesis/program-city.json',
|
|
27
|
+
'.genesis/call-web.json',
|
|
27
28
|
'.codex/hooks.json',
|
|
28
29
|
'.claude/settings.json',
|
|
29
30
|
'.opencode/plugins/genesis-project-guidance.js',
|
|
@@ -36,11 +37,13 @@ export const GENESIS_BOOTSTRAP_PATHS = Object.freeze([
|
|
|
36
37
|
]);
|
|
37
38
|
const bootstrapPaths = new Set(GENESIS_BOOTSTRAP_PATHS);
|
|
38
39
|
// Genesis creates .gitignore only to ignore its Cities; any other content is the project's.
|
|
40
|
+
// Earlier releases wrote fewer derived-artifact lines; any such subset is still Genesis's.
|
|
39
41
|
async function isBootstrapGitignore(projectRoot, paths) {
|
|
40
42
|
if (!paths.includes('.gitignore')) return false;
|
|
41
43
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const lines = (await readFile(path.join(projectRoot, '.gitignore'), 'utf8')).split('\n');
|
|
45
|
+
return lines.pop() === '' && lines.length > 0 && new Set(lines).size === lines.length
|
|
46
|
+
&& lines.every(line => GENESIS_DERIVED_ARTIFACTS_GITIGNORE_LINES.includes(line));
|
|
44
47
|
} catch (error) {
|
|
45
48
|
if (['ENOENT', 'ENOTDIR', 'EISDIR'].includes(error?.code)) return false;
|
|
46
49
|
throw error;
|
package/src/index/prompt.js
CHANGED
|
@@ -167,6 +167,7 @@ function collaborationPromptContext(collaboration) {
|
|
|
167
167
|
responseLength: collaboration.responseLength,
|
|
168
168
|
experience: collaboration.experience,
|
|
169
169
|
explanationStyle: collaboration.explanationStyle,
|
|
170
|
+
longChecks: collaboration.longChecks || 'unset',
|
|
170
171
|
requirements: collaboration.requirements || 'Nothing.',
|
|
171
172
|
};
|
|
172
173
|
}
|
package/src/index/utils.js
CHANGED
|
@@ -25,6 +25,40 @@ export function stableJson(value, { space = 2 } = {}) {
|
|
|
25
25
|
return `${JSON.stringify(stable(value), null, space)}\n`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const omittedJson = value => value === undefined || typeof value === 'function' || typeof value === 'symbol';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The text of stableJson(value, { space: 0 }) as consecutive parts. Members of
|
|
32
|
+
* the top two object levels and elements of the top three array levels are
|
|
33
|
+
* separate parts, so a large document is never copied or held whole.
|
|
34
|
+
*/
|
|
35
|
+
export function* stableJsonParts(value, depth = 0) {
|
|
36
|
+
if (Array.isArray(value) && depth < 3) {
|
|
37
|
+
yield '[';
|
|
38
|
+
for (let index = 0; index < value.length; index++) {
|
|
39
|
+
if (index) yield ',';
|
|
40
|
+
if (omittedJson(value[index])) yield 'null';
|
|
41
|
+
else yield* stableJsonParts(value[index], depth + 1);
|
|
42
|
+
}
|
|
43
|
+
yield ']';
|
|
44
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value) && depth < 2) {
|
|
45
|
+
// The member order of the copy stable() builds, including integer-like keys.
|
|
46
|
+
const sorted = Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]]));
|
|
47
|
+
let separator = '';
|
|
48
|
+
yield '{';
|
|
49
|
+
for (const [key, item] of Object.entries(sorted)) {
|
|
50
|
+
if (omittedJson(item)) continue;
|
|
51
|
+
yield `${separator}${JSON.stringify(key)}:`;
|
|
52
|
+
separator = ',';
|
|
53
|
+
yield* stableJsonParts(item, depth + 1);
|
|
54
|
+
}
|
|
55
|
+
yield '}';
|
|
56
|
+
} else {
|
|
57
|
+
yield JSON.stringify(stable(value));
|
|
58
|
+
}
|
|
59
|
+
if (depth === 0) yield '\n';
|
|
60
|
+
}
|
|
61
|
+
|
|
28
62
|
export function uniqueSorted(values = []) {
|
|
29
63
|
return [...new Set(values)].sort();
|
|
30
64
|
}
|
package/src/index.js
CHANGED
|
@@ -202,9 +202,10 @@ export function indexCodebase({
|
|
|
202
202
|
stackPackages = [],
|
|
203
203
|
write = true,
|
|
204
204
|
observations = null,
|
|
205
|
+
callWeb = false,
|
|
205
206
|
signal,
|
|
206
207
|
} = {}) {
|
|
207
|
-
return buildProjectIndex({ projectRoot, queries, stackPackages, write, observations, signal });
|
|
208
|
+
return buildProjectIndex({ projectRoot, queries, stackPackages, write, observations, callWeb, signal });
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
export function inspectDerivedArtifacts() {
|
|
@@ -246,6 +247,7 @@ export { inspectSubsystems } from './index/subsystems.js';
|
|
|
246
247
|
export { inspectOpenCodePlugin, installOpenCodePlugin as syncOpenCodePlugin } from './index/opencode-plugin.js';
|
|
247
248
|
|
|
248
249
|
export { ARCHITECTURE_OBSERVATIONS_SCHEMA, validateArchitectureObservations, connectArchitectureObservations, mergeArchitectureObservations } from './index/architecture-observations.js';
|
|
250
|
+
export { CALL_WEB_SCHEMA, validateCallWeb, readCallWeb } from './index/call-web.js';
|
|
249
251
|
|
|
250
252
|
export { validateArchitectureFacts } from './index/architecture.js';
|
|
251
253
|
export { CITY_SCHEMA_VERSION, MACHINE_CITY_SCHEMA, PROGRAM_CITY_SCHEMA, validateCityDocument } from './index/city-documents.js';
|