aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
187 lines (162 loc) • 5.61 kB
JavaScript
;
const path = require('path');
const { RegistryProvider } = require('./providers/registry-provider');
/** Cached provider instance (survives across hook invocations in same process). */
let _provider = null;
let _providerRoot = null;
/**
* Get or create a RegistryProvider singleton.
* Resets if projectRoot changes between calls.
* @param {string} projectRoot - Project root directory
* @returns {RegistryProvider}
*/
function getProvider(projectRoot) {
if (!_provider || _providerRoot !== projectRoot) {
_provider = new RegistryProvider({ projectRoot });
_providerRoot = projectRoot;
}
return _provider;
}
/**
* Resolve code intelligence context for a file being written/edited.
*
* Queries RegistryProvider for:
* - Entity definition (path, layer, purpose, type)
* - References (files that use this entity)
* - Dependencies (entities this file depends on)
*
* @param {string} filePath - Absolute or relative path to the target file
* @param {string} cwd - Project root / working directory
* @returns {{ entity: Object|null, references: Array|null, dependencies: Object|null }|null}
*/
async function resolveCodeIntel(filePath, cwd) {
if (!filePath || !cwd) return null;
try {
const provider = getProvider(cwd);
if (!provider.isAvailable()) return null;
// Normalize to relative path (registry uses relative paths)
let relativePath = filePath;
if (path.isAbsolute(filePath)) {
relativePath = path.relative(cwd, filePath).replace(/\\/g, '/');
} else {
relativePath = filePath.replace(/\\/g, '/');
}
// Run all three queries in parallel
const [definition, references, dependencies] = await Promise.all([
provider.findDefinition(relativePath),
provider.findReferences(relativePath),
provider.analyzeDependencies(relativePath),
]);
// Treat empty dependency graph as no data
const hasUsefulDeps = dependencies && dependencies.nodes && dependencies.nodes.length > 0;
// If nothing found at all, try searching by the file basename
if (!definition && !references && !hasUsefulDeps) {
const basename = path.basename(relativePath, path.extname(relativePath));
const fallbackDef = await provider.findDefinition(basename);
if (!fallbackDef) return null;
const [fallbackRefs, fallbackDeps] = await Promise.all([
provider.findReferences(basename),
provider.analyzeDependencies(basename),
]);
return {
entity: fallbackDef,
references: fallbackRefs,
dependencies: fallbackDeps,
};
}
return {
entity: definition,
references,
dependencies,
};
} catch (_err) {
// Guard against provider exceptions to avoid unhandled rejections in hook runtime
return null;
}
}
/**
* Format code intelligence data as XML for injection into Claude context.
*
* @param {Object|null} intel - Result from resolveCodeIntel()
* @param {string} filePath - Target file path (for display)
* @returns {string|null} XML string or null if no data
*/
function formatAsXml(intel, filePath) {
if (!intel) return null;
const { entity, references, dependencies } = intel;
// At least one piece of data must exist
if (!entity && !references && !dependencies) return null;
const lines = ['<code-intel-context>'];
lines.push(` <target-file>${escapeXml(filePath)}</target-file>`);
// Entity definition
if (entity) {
lines.push(' <existing-entity>');
if (entity.file) lines.push(` <path>${escapeXml(entity.file)}</path>`);
if (entity.context) lines.push(` <purpose>${escapeXml(entity.context)}</purpose>`);
lines.push(' </existing-entity>');
}
// References
if (references && references.length > 0) {
// Deduplicate by file path
const uniqueRefs = [];
const seen = new Set();
for (const ref of references) {
if (ref.file && !seen.has(ref.file)) {
seen.add(ref.file);
uniqueRefs.push(ref);
}
}
lines.push(` <referenced-by count="${uniqueRefs.length}">`);
for (const ref of uniqueRefs.slice(0, 15)) {
const ctx = ref.context ? ` context="${escapeXml(ref.context)}"` : '';
lines.push(` <ref file="${escapeXml(ref.file)}"${ctx} />`);
}
if (uniqueRefs.length > 15) {
lines.push(` <!-- ...and ${uniqueRefs.length - 15} more -->`);
}
lines.push(' </referenced-by>');
}
// Dependencies
if (dependencies && dependencies.nodes && dependencies.nodes.length > 1) {
// First node is the target itself, rest are dependencies
const depNodes = dependencies.nodes.slice(1);
lines.push(` <dependencies count="${depNodes.length}">`);
for (const dep of depNodes.slice(0, 10)) {
const layer = dep.layer ? ` layer="${dep.layer}"` : '';
lines.push(` <dep name="${escapeXml(dep.name)}"${layer} />`);
}
if (depNodes.length > 10) {
lines.push(` <!-- ...and ${depNodes.length - 10} more -->`);
}
lines.push(' </dependencies>');
}
lines.push('</code-intel-context>');
return lines.join('\n');
}
/**
* Escape special XML characters.
* @param {string} str
* @returns {string}
*/
function escapeXml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
* Reset cached provider (for testing).
*/
function _resetForTesting() {
_provider = null;
_providerRoot = null;
}
module.exports = {
resolveCodeIntel,
formatAsXml,
escapeXml,
getProvider,
_resetForTesting,
};