Files
dev-intel-v2/contracts.js

248 lines
8.0 KiB
JavaScript
Raw Normal View History

const fs = require('fs');
const path = require('path');
const Parser = require('tree-sitter');
const tsGrammar = require('tree-sitter-typescript').typescript;
/**
* Phase 7B: Contract Extractor
* Extracts interfaces, type aliases, and enums from TypeScript source files.
* Uses tree-sitter AST parsing zero inference.
*/
const parser = new Parser();
parser.setLanguage(tsGrammar);
/** Extract contracts from a single TypeScript source file */
function extractContracts(filePath, relFile) {
const source = fs.readFileSync(filePath, 'utf8');
const tree = parser.parse(source);
const contracts = [];
for (const node of tree.rootNode.children) {
// Handle both exported and non-exported declarations
let decl = node;
let isExported = false;
if (node.type === 'export_statement') {
isExported = true;
// The actual declaration is a child
decl = node.namedChildren.find(c =>
c.type === 'interface_declaration' ||
c.type === 'type_alias_declaration' ||
c.type === 'enum_declaration'
);
if (!decl) continue;
}
if (decl.type === 'interface_declaration') {
const name = decl.childForFieldName('name')?.text;
if (!name) continue;
const contract = {
id: `${relFile}:${name}`,
type: 'Interface',
name,
visibility: isExported ? 'public' : 'private',
};
// Extract extends clause
const heritage = decl.children.find(c => c.type === 'extends_type_clause');
if (heritage) {
contract.extends = [];
for (const child of heritage.namedChildren) {
const typeName = child.type === 'type_identifier' ? child.text : child.text;
if (typeName && typeName !== 'extends') contract.extends.push(typeName);
}
}
// Extract fields from interface body
const body = decl.childForFieldName('body');
if (body) {
contract.fields = [];
for (const member of body.namedChildren) {
if (member.type === 'property_signature' || member.type === 'public_field_definition') {
const fieldName = member.childForFieldName('name')?.text;
const typeAnnotation = member.childForFieldName('type') || member.children.find(c => c.type === 'type_annotation');
let fieldType = 'unknown';
if (typeAnnotation) {
// type_annotation wraps the actual type; strip the ': '
fieldType = typeAnnotation.text.replace(/^:\s*/, '');
}
if (fieldName) {
contract.fields.push({ name: fieldName, type: fieldType });
}
} else if (member.type === 'method_signature') {
const methodName = member.childForFieldName('name')?.text;
if (methodName) {
contract.fields.push({ name: methodName, type: 'method' });
}
}
}
}
contracts.push(contract);
} else if (decl.type === 'type_alias_declaration') {
const name = decl.childForFieldName('name')?.text;
if (!name) continue;
contracts.push({
id: `${relFile}:${name}`,
type: 'TypeAlias',
name,
visibility: isExported ? 'public' : 'private',
});
} else if (decl.type === 'enum_declaration') {
const name = decl.childForFieldName('name')?.text;
if (!name) continue;
const contract = {
id: `${relFile}:${name}`,
type: 'Enum',
name,
visibility: isExported ? 'public' : 'private',
members: [],
};
const body = decl.childForFieldName('body');
if (body) {
for (const member of body.namedChildren) {
if (member.type === 'enum_assignment' || member.type === 'enum_member') {
// enum_assignment: "Debug = 'debug'" — first named child is the identifier
const nameNode = member.namedChildren[0];
const memberName = nameNode?.text || member.childForFieldName('name')?.text;
if (memberName) contract.members.push(memberName);
} else if (member.type === 'property_identifier') {
if (member.text) contract.members.push(member.text);
}
}
}
contracts.push(contract);
}
}
return contracts;
}
/**
* Extract contracts from all TypeScript files in a subsystem map.
* @param {object} subsystemMap - Result from buildSubsystems
* @param {string} srcRoot - Absolute path to source root
* @returns {object} { contracts, bySubsystem }
*/
function extractAllContracts(subsystemMap, srcRoot) {
const allContracts = [];
const bySubsystem = {};
for (const sub of subsystemMap.subsystems) {
bySubsystem[sub.name] = [];
for (const relFile of sub.files) {
if (!relFile.endsWith('.ts') && !relFile.endsWith('.tsx')) continue;
const absPath = path.join(srcRoot, relFile);
if (!fs.existsSync(absPath)) continue;
try {
const contracts = extractContracts(absPath, relFile);
allContracts.push(...contracts);
bySubsystem[sub.name].push(...contracts);
} catch (err) {
// Skip files that fail to parse
}
}
}
return { contracts: allContracts, bySubsystem };
}
/**
* Build a cross-reference map: which contracts are used by which subsystems.
* @param {Array} contracts - All extracted contracts
* @param {object} graph - Graph from GraphStore
* @param {function} relPathFn - relPath function from subsystem.js
* @returns {object} Map of contract name { definedIn, usedBy[] }
*/
function buildContractXref(contracts, graph, relPathFn) {
const xref = {};
// Index contract names → definition location
for (const c of contracts) {
xref[c.name] = {
id: c.id,
type: c.type,
definedIn: c.id.split(':')[0].split('/')[0],
usedBy: new Set(),
};
}
// Scan IMPORTS edges for contract references
for (const e of graph.edges) {
if (e.type !== 'IMPORTS') continue;
const dep = e.target.replace('dep:', '');
// Check if any contract is defined in the imported file
for (const c of contracts) {
const contractFile = c.id.split(':')[0];
const contractFileNoExt = contractFile.replace(/\.\w+$/, '');
if (dep === contractFile || dep === contractFileNoExt || dep.endsWith('/' + contractFile) || dep.endsWith('/' + contractFileNoExt)) {
const sourceFile = relPathFn(e.source.split(':')[0]);
const sourceSub = sourceFile.split('/')[0];
if (sourceSub !== xref[c.name].definedIn) {
xref[c.name].usedBy.add(sourceSub);
}
}
}
}
// Convert Sets to Arrays
for (const key of Object.keys(xref)) {
xref[key].usedBy = Array.from(xref[key].usedBy);
}
return xref;
}
if (require.main === module) {
const srcRoot = process.argv[2];
if (!srcRoot) {
console.error('Usage: node contracts.js <srcRoot>');
process.exit(1);
}
// Quick standalone mode: scan all .ts files under srcRoot
const { buildSubsystems, relPath } = require('./subsystem.js');
const GraphStore = require('./graph.js');
// Need a snapshot for subsystem map
const snapshotPath = process.argv[3];
if (snapshotPath) {
const graph = GraphStore.loadSnapshot(snapshotPath);
const subs = buildSubsystems(graph);
const result = extractAllContracts(subs, srcRoot);
console.log(JSON.stringify(result, null, 2));
} else {
// Just scan the directory
const files = [];
function walk(dir, rel) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
const r = rel ? `${rel}/${entry.name}` : entry.name;
if (entry.isDirectory()) walk(full, r);
else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
files.push({ abs: full, rel: r });
}
}
}
walk(srcRoot, '');
const all = [];
for (const f of files) {
all.push(...extractContracts(f.abs, f.rel));
}
console.log(JSON.stringify({ contracts: all }, null, 2));
}
}
module.exports = { extractContracts, extractAllContracts, buildContractXref };