@polycode-projects/the-mechanical-code-talker 2.11.12 → 3.0.1
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 +4 -2
- package/bin/tmct.mjs +62 -2
- package/corpus/tier2/generate.mjs +1 -1
- package/package.json +3 -2
- package/src/adapters/source.mjs +8 -6
- package/src/adapters/toml-config.mjs +8 -0
- package/src/domain/cli-verbs.mjs +9 -0
- package/src/domain/codeplan/graph-delta.mjs +239 -0
- package/src/domain/codeplan/graph-predicates.mjs +0 -0
- package/src/domain/codeplan/operators.mjs +232 -0
- package/src/domain/codeplan/planner.mjs +87 -0
- package/src/index/extract-jsts.mjs +251 -0
- package/src/index/extract-python.mjs +46 -0
- package/src/index/extract_ast.py +364 -0
- package/src/index/index-repo.mjs +173 -0
- package/src/index/registry.mjs +37 -0
- package/src/index/spawn.mjs +35 -0
- package/src/index/walk.mjs +0 -0
- package/src/services/chat-session.mjs +10 -3
- package/src/services/chat.mjs +5 -5
- package/src/services/sessions.mjs +3 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +89 -89
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// planner.mjs — bounded BFS to a graph-predicate goal (PLAN_CODE.md §3.3). The
|
|
2
|
+
// goal is a set of predicates over graph shape, the same species as
|
|
3
|
+
// domain.mjs's compileGoal specs: "entity X is titled parseRow; X lives in
|
|
4
|
+
// module M; every former call site imports M." findActionPath searches operator
|
|
5
|
+
// applications (operators.mjs's codeGraphMoves) over projected graph snapshots,
|
|
6
|
+
// keyed by the path-independent canonicalStateKey, shortest plan first, honest
|
|
7
|
+
// miss on exhaustion. No new search engine, no I/O.
|
|
8
|
+
|
|
9
|
+
import { findActionPath } from "../planning.mjs";
|
|
10
|
+
import { canonicalStateKey } from "./graph-delta.mjs";
|
|
11
|
+
import { moduleDefining } from "./graph-predicates.mjs";
|
|
12
|
+
import { CODE_OPERATORS, codeGraphMoves } from "./operators.mjs";
|
|
13
|
+
|
|
14
|
+
const str = (value) => String(value ?? "");
|
|
15
|
+
|
|
16
|
+
const titleOf = (state, id) => state.entities.find((e) => e.id === id)?.title;
|
|
17
|
+
const hasEntity = (state, id) => state.entities.some((e) => e.id === id);
|
|
18
|
+
const hasEdge = (state, { subject, predicate, object }) =>
|
|
19
|
+
state.edges.some((r) => r.subject === subject && r.predicate === predicate && r.object === object);
|
|
20
|
+
|
|
21
|
+
/** The closed goal-predicate vocabulary: one checker per `kind`, each a pure
|
|
22
|
+
* boolean over a state. A goal spec outside this set is a programming error. */
|
|
23
|
+
export const GOAL_PREDICATES = Object.freeze({
|
|
24
|
+
"entity-titled": (state, g) => titleOf(state, g.id) === str(g.title),
|
|
25
|
+
"entity-in-module": (state, g) => moduleDefining(state, g.id) === str(g.moduleId),
|
|
26
|
+
"entity-absent": (state, g) => !hasEntity(state, g.id),
|
|
27
|
+
"edge-present": (state, g) => hasEdge(state, { subject: str(g.subject), predicate: str(g.predicate), object: str(g.object) }),
|
|
28
|
+
"edge-absent": (state, g) => !hasEdge(state, { subject: str(g.subject), predicate: str(g.predicate), object: str(g.object) }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Compile goal specs into a single state predicate — the conjunction of every
|
|
32
|
+
* spec's checker. Throws on an unknown `kind`. */
|
|
33
|
+
export function compileCodeGoal(goalSpecs) {
|
|
34
|
+
const specs = goalSpecs || [];
|
|
35
|
+
for (const g of specs) {
|
|
36
|
+
if (!GOAL_PREDICATES[str(g.kind)]) throw new Error(`unknown goal predicate kind ${JSON.stringify(str(g.kind))}`);
|
|
37
|
+
}
|
|
38
|
+
return function isGoal(state) {
|
|
39
|
+
return specs.every((g) => GOAL_PREDICATES[str(g.kind)](state, g));
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Derive the grounders' parameter pool from the goal (operators.mjs's
|
|
44
|
+
* `context`): rename titles from entity-titled specs, move/create-module target
|
|
45
|
+
* modules from entity-in-module specs, delete targets from entity-absent specs.
|
|
46
|
+
* A goal-directed pool keeps operator enumeration bounded — the planner only
|
|
47
|
+
* proposes renames/moves the goal actually calls for. A target module carries a
|
|
48
|
+
* title (spec `moduleTitle`, else its id with the `mod:` prefix stripped) so
|
|
49
|
+
* create-module can mint it. */
|
|
50
|
+
export function deriveContext(goalSpecs) {
|
|
51
|
+
const titles = new Set();
|
|
52
|
+
const moduleTargets = new Map();
|
|
53
|
+
const deleteTargets = new Set();
|
|
54
|
+
for (const g of goalSpecs || []) {
|
|
55
|
+
if (g.kind === "entity-titled") titles.add(str(g.title));
|
|
56
|
+
else if (g.kind === "entity-in-module") {
|
|
57
|
+
const id = str(g.moduleId);
|
|
58
|
+
if (!moduleTargets.has(id)) moduleTargets.set(id, { id, title: str(g.moduleTitle) || id.replace(/^mod:/, "") });
|
|
59
|
+
} else if (g.kind === "entity-absent") deleteTargets.add(str(g.id));
|
|
60
|
+
}
|
|
61
|
+
return { titles: [...titles], moduleTargets: [...moduleTargets.values()], deleteTargets: [...deleteTargets] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Plan a code change: search catalogue-operator applications from `startState`
|
|
66
|
+
* to a state satisfying `goalSpecs`. Returns `{ actions, states, plan }` — the
|
|
67
|
+
* ordered moves, the snapshots between them, and a per-step receipt (operator,
|
|
68
|
+
* binding, declared effect) — or `null` on an honest miss (no plan within
|
|
69
|
+
* `maxDepth`). Deterministic: the same fixture, catalogue and goal always yield
|
|
70
|
+
* the same plan.
|
|
71
|
+
*/
|
|
72
|
+
export function planCodeChange(startState, goalSpecs, { catalogue = CODE_OPERATORS, maxDepth = 12 } = {}) {
|
|
73
|
+
const isGoal = compileCodeGoal(goalSpecs);
|
|
74
|
+
const context = deriveContext(goalSpecs);
|
|
75
|
+
const applyActions = (state) => codeGraphMoves(state, context, { catalogue });
|
|
76
|
+
const found = findActionPath(startState, isGoal, applyActions, { maxDepth, stateKey: canonicalStateKey });
|
|
77
|
+
if (!found) return null;
|
|
78
|
+
const plan = found.actions.map((action, i) => ({
|
|
79
|
+
operator: action.name,
|
|
80
|
+
binding: action.binding,
|
|
81
|
+
effects: action.effects,
|
|
82
|
+
label: action.label,
|
|
83
|
+
before: found.states[i],
|
|
84
|
+
after: found.states[i + 1],
|
|
85
|
+
}));
|
|
86
|
+
return { actions: found.actions, states: found.states, plan };
|
|
87
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// JS/TS extractor — TypeScript compiler API (direct `typescript` dep). Uses
|
|
2
|
+
// ts.createSourceFile per file (no Program / no type-checker): the fast,
|
|
3
|
+
// deterministic, offline structural pass. Covers .ts/.tsx/.js/.jsx/.mjs/.cjs.
|
|
4
|
+
// Emits the `{path,dotted,imports,defines,calls,exports}` contract
|
|
5
|
+
// graph-build.mjs's buildEntities() consumes.
|
|
6
|
+
//
|
|
7
|
+
// Both module systems are read: ESM import/export declarations AND a CommonJS
|
|
8
|
+
// pass (top-level CJS require calls with literal specifiers → imports;
|
|
9
|
+
// module.exports / exports.name assignments → exports) — CJS-only repos were
|
|
10
|
+
// producing edge-empty graphs before the CJS pass existed.
|
|
11
|
+
//
|
|
12
|
+
// Fidelity note: params/returns are ANNOTATION strings (Group-A mechanical), not
|
|
13
|
+
// resolved types — the same honesty the Python path keeps ("returns = annotation
|
|
14
|
+
// only"). A type-RESOLVED pass (real Program + checker) is a research horizon,
|
|
15
|
+
// not run here.
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import ts from "typescript";
|
|
19
|
+
import { walk, relPath } from "./walk.mjs";
|
|
20
|
+
|
|
21
|
+
const EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
22
|
+
const stripExt = (p) => p.replace(/\.(tsx?|jsx?|mjs|cjs)$/i, "");
|
|
23
|
+
|
|
24
|
+
function scriptKind(path) {
|
|
25
|
+
if (/\.tsx$/i.test(path)) return ts.ScriptKind.TSX;
|
|
26
|
+
if (/\.ts$/i.test(path)) return ts.ScriptKind.TS;
|
|
27
|
+
if (/\.jsx$/i.test(path)) return ts.ScriptKind.JSX;
|
|
28
|
+
return ts.ScriptKind.JS;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lineOf = (sf, node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
32
|
+
const endLineOf = (sf, node) => sf.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
33
|
+
|
|
34
|
+
/** "(a: string, b = 3)" → "a: string, b = 3" (annotation strings; not resolved). */
|
|
35
|
+
function paramsText(sf, node) {
|
|
36
|
+
const ps = node.parameters || [];
|
|
37
|
+
return ps.map((p) => p.getText(sf).replace(/\s+/g, " ").trim()).join(", ").slice(0, 160);
|
|
38
|
+
}
|
|
39
|
+
function returnText(sf, node) {
|
|
40
|
+
return node.type ? node.type.getText(sf).replace(/\s+/g, " ").trim().slice(0, 80) : "";
|
|
41
|
+
}
|
|
42
|
+
function firstDocLine(sf, node) {
|
|
43
|
+
const ranges = ts.getLeadingCommentRanges(sf.text, node.getFullStart()) || [];
|
|
44
|
+
for (const r of ranges) {
|
|
45
|
+
const raw = sf.text.slice(r.pos, r.end);
|
|
46
|
+
const m = raw.replace(/^\/\*\*?|\*\/$/g, "").split("\n").map((l) => l.replace(/^\s*\*?\s?/, "").trim()).find(Boolean);
|
|
47
|
+
if (m) return m.slice(0, 120);
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
const hasMod = (node, kind) => (node.modifiers || []).some((m) => m.kind === kind);
|
|
52
|
+
function visibilityOf(node) {
|
|
53
|
+
if (hasMod(node, ts.SyntaxKind.PrivateKeyword)) return "private";
|
|
54
|
+
if (hasMod(node, ts.SyntaxKind.ProtectedKeyword)) return "protected";
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
function decoratorsOf(sf, node) {
|
|
58
|
+
const ds = ts.canHaveDecorators?.(node) ? ts.getDecorators?.(node) : null;
|
|
59
|
+
return (ds || []).map((d) => d.expression.getText(sf).replace(/\s+/g, " ").slice(0, 80));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Collect callee names within a node body (coarse; unparsed call target). */
|
|
63
|
+
function callsIn(sf, node) {
|
|
64
|
+
const out = new Set();
|
|
65
|
+
const visit = (n) => {
|
|
66
|
+
if (ts.isCallExpression(n)) {
|
|
67
|
+
const t = n.expression;
|
|
68
|
+
let name = "";
|
|
69
|
+
if (ts.isIdentifier(t)) name = t.text;
|
|
70
|
+
else if (ts.isPropertyAccessExpression(t)) name = t.getText(sf);
|
|
71
|
+
if (name) out.add(name.slice(0, 80));
|
|
72
|
+
}
|
|
73
|
+
ts.forEachChild(n, visit);
|
|
74
|
+
};
|
|
75
|
+
ts.forEachChild(node, visit);
|
|
76
|
+
return [...out].sort();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function extractFile(absPath, root) {
|
|
80
|
+
const text = await readFile(absPath, "utf8").catch(() => null);
|
|
81
|
+
if (text == null) return { failed: true, path: relPath(root, absPath) };
|
|
82
|
+
const path = relPath(root, absPath);
|
|
83
|
+
let sf;
|
|
84
|
+
try {
|
|
85
|
+
sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind(path));
|
|
86
|
+
} catch {
|
|
87
|
+
return { failed: true, path };
|
|
88
|
+
}
|
|
89
|
+
const dir = dirname(path);
|
|
90
|
+
const imports = new Set();
|
|
91
|
+
const calls = new Set();
|
|
92
|
+
const defines = [];
|
|
93
|
+
const exports = new Set();
|
|
94
|
+
|
|
95
|
+
const resolveSpecifier = (spec) => {
|
|
96
|
+
if (!spec || !spec.startsWith(".")) { if (spec) imports.add(spec); return; } // bare/external
|
|
97
|
+
const joined = join(dir, spec).split(/[\\/]/).join("/");
|
|
98
|
+
const base = stripExt(joined);
|
|
99
|
+
imports.add(base);
|
|
100
|
+
imports.add(`${base}/index`); // dir-import fallback (./foo → ./foo/index)
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// CommonJS pass: CJS require / module.exports repos were leaving graphs
|
|
104
|
+
// edge-empty. Top-level statements only, STRICTLY literal arguments — a
|
|
105
|
+
// computed specifier is skipped, never guessed (the house "no wrong edge" rule).
|
|
106
|
+
/** A CJS require call with a single literal arg → its specifier, else null. */
|
|
107
|
+
const requireSpecifier = (node) =>
|
|
108
|
+
node && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require" &&
|
|
109
|
+
node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0])
|
|
110
|
+
? node.arguments[0].text
|
|
111
|
+
: null;
|
|
112
|
+
/** Peel a `.member` access / parens down to the require call (never enters functions). */
|
|
113
|
+
const requireIn = (expr) => {
|
|
114
|
+
let e = expr;
|
|
115
|
+
while (e && (ts.isPropertyAccessExpression(e) || ts.isParenthesizedExpression(e) || ts.isNonNullExpression?.(e))) e = e.expression;
|
|
116
|
+
return requireSpecifier(e);
|
|
117
|
+
};
|
|
118
|
+
const isModuleExports = (e) =>
|
|
119
|
+
ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === "module" && e.name.text === "exports";
|
|
120
|
+
const isExportsRef = (e) => isModuleExports(e) || (ts.isIdentifier(e) && e.text === "exports");
|
|
121
|
+
|
|
122
|
+
const addFn = (name, node, kind, extra = {}) => {
|
|
123
|
+
defines.push({
|
|
124
|
+
name, kind, lineno: lineOf(sf, node), end_lineno: endLineOf(sf, node),
|
|
125
|
+
decorators: decoratorsOf(sf, node),
|
|
126
|
+
params: paramsText(sf, node), returns: returnText(sf, node),
|
|
127
|
+
calls: callsIn(sf, node), doc: firstDocLine(sf, node),
|
|
128
|
+
...(visibilityOf(node) ? { visibility: visibilityOf(node) } : {}),
|
|
129
|
+
...extra,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const stmt of sf.statements) {
|
|
134
|
+
// imports / re-exports
|
|
135
|
+
if (ts.isImportDeclaration(stmt) && stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
|
|
136
|
+
resolveSpecifier(stmt.moduleSpecifier.text);
|
|
137
|
+
} else if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
|
|
138
|
+
resolveSpecifier(stmt.moduleSpecifier.text);
|
|
139
|
+
}
|
|
140
|
+
const exported = hasMod(stmt, ts.SyntaxKind.ExportKeyword);
|
|
141
|
+
|
|
142
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
143
|
+
addFn(stmt.name.text, stmt, "function");
|
|
144
|
+
if (exported) exports.add(stmt.name.text);
|
|
145
|
+
} else if (ts.isClassDeclaration(stmt) && stmt.name) {
|
|
146
|
+
const cname = stmt.name.text;
|
|
147
|
+
const bases = [];
|
|
148
|
+
for (const h of stmt.heritageClauses || []) for (const t of h.types) bases.push(t.getText(sf).slice(0, 80));
|
|
149
|
+
defines.push({
|
|
150
|
+
name: cname, kind: "class", lineno: lineOf(sf, stmt), end_lineno: endLineOf(sf, stmt),
|
|
151
|
+
bases, decorators: decoratorsOf(sf, stmt), doc: firstDocLine(sf, stmt),
|
|
152
|
+
});
|
|
153
|
+
if (exported) exports.add(cname);
|
|
154
|
+
for (const mem of stmt.members) {
|
|
155
|
+
if (ts.isMethodDeclaration(mem) || ts.isConstructorDeclaration(mem) ||
|
|
156
|
+
ts.isGetAccessor(mem) || ts.isSetAccessor(mem)) {
|
|
157
|
+
const mn = mem.name ? mem.name.getText(sf) : "constructor";
|
|
158
|
+
addFn(`${cname}.${mn}`, mem, "method");
|
|
159
|
+
} else if (ts.isPropertyDeclaration(mem) && mem.name) {
|
|
160
|
+
defines.push({
|
|
161
|
+
name: `${cname}.${mem.name.getText(sf)}`, kind: "attribute",
|
|
162
|
+
lineno: lineOf(sf, mem), end_lineno: endLineOf(sf, mem), decorators: decoratorsOf(sf, mem),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (ts.isInterfaceDeclaration(stmt) && stmt.name) {
|
|
167
|
+
// interfaces map to Class nodes (type surface) — useful for TS-heavy repos
|
|
168
|
+
defines.push({
|
|
169
|
+
name: stmt.name.text, kind: "class", subkind: "interface",
|
|
170
|
+
lineno: lineOf(sf, stmt), end_lineno: endLineOf(sf, stmt),
|
|
171
|
+
bases: (stmt.heritageClauses || []).flatMap((h) => h.types.map((t) => t.getText(sf).slice(0, 80))),
|
|
172
|
+
decorators: [], doc: firstDocLine(sf, stmt),
|
|
173
|
+
});
|
|
174
|
+
if (exported) exports.add(stmt.name.text);
|
|
175
|
+
} else if (ts.isVariableStatement(stmt)) {
|
|
176
|
+
const isConst = (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
177
|
+
for (const d of stmt.declarationList.declarations) {
|
|
178
|
+
// CJS import: const X = <require 'spec'> / const {a} = <require 'spec'> /
|
|
179
|
+
// const Y = <require 'spec'>.member — BEFORE the identifier-only gate so a
|
|
180
|
+
// destructuring binding still records the import edge.
|
|
181
|
+
const reqSpec = requireIn(d.initializer);
|
|
182
|
+
if (reqSpec != null) resolveSpecifier(reqSpec);
|
|
183
|
+
if (!ts.isIdentifier(d.name)) continue;
|
|
184
|
+
const vn = d.name.text;
|
|
185
|
+
const init = d.initializer;
|
|
186
|
+
// arrow/function assigned to a const → treat as a function define
|
|
187
|
+
if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
|
|
188
|
+
addFn(vn, init, "function");
|
|
189
|
+
} else if (isConst && /^[A-Z0-9_]+$/.test(vn)) {
|
|
190
|
+
defines.push({ name: vn, kind: "global", lineno: lineOf(sf, stmt), end_lineno: endLineOf(sf, stmt),
|
|
191
|
+
decorators: [], value: (init ? init.getText(sf) : "").replace(/\s+/g, " ").slice(0, 80), is_constant: true });
|
|
192
|
+
} else if (init && ts.isCallExpression(init)) {
|
|
193
|
+
// live-object global (e.g. const router = Router()) — a registration anchor
|
|
194
|
+
defines.push({ name: vn, kind: "global", lineno: lineOf(sf, stmt), end_lineno: endLineOf(sf, stmt),
|
|
195
|
+
decorators: [], value: init.getText(sf).replace(/\s+/g, " ").slice(0, 80) });
|
|
196
|
+
}
|
|
197
|
+
if (exported) exports.add(vn);
|
|
198
|
+
}
|
|
199
|
+
} else if (ts.isExportAssignment?.(stmt)) {
|
|
200
|
+
exports.add("default");
|
|
201
|
+
} else if (ts.isExpressionStatement(stmt)) {
|
|
202
|
+
// CJS exports + side-effect/re-export requires. Walk `=` chains so
|
|
203
|
+
// `exports = module.exports = X` records one default export, and
|
|
204
|
+
// `module.exports = <require './lib'>` records both the export and the import.
|
|
205
|
+
let expr = stmt.expression;
|
|
206
|
+
while (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
207
|
+
const lhs = expr.left;
|
|
208
|
+
if (isExportsRef(lhs)) exports.add("default");
|
|
209
|
+
else if (ts.isPropertyAccessExpression(lhs) && isExportsRef(lhs.expression)) {
|
|
210
|
+
exports.add(lhs.name.text);
|
|
211
|
+
}
|
|
212
|
+
expr = expr.right;
|
|
213
|
+
}
|
|
214
|
+
const reqSpec = requireIn(expr); // covers a bare side-effect require too
|
|
215
|
+
if (reqSpec != null) resolveSpecifier(reqSpec);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// module-level coarse calls (whole file)
|
|
220
|
+
const visit = (n) => {
|
|
221
|
+
if (ts.isCallExpression(n)) {
|
|
222
|
+
const t = n.expression;
|
|
223
|
+
let name = "";
|
|
224
|
+
if (ts.isIdentifier(t)) name = t.text;
|
|
225
|
+
else if (ts.isPropertyAccessExpression(t)) name = t.getText(sf);
|
|
226
|
+
if (name) calls.add(name.slice(0, 80));
|
|
227
|
+
}
|
|
228
|
+
ts.forEachChild(n, visit);
|
|
229
|
+
};
|
|
230
|
+
ts.forEachChild(sf, visit);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
path, dotted: stripExt(path),
|
|
234
|
+
imports: [...imports].sort(), defines, calls: [...calls].sort(), exports: [...exports].sort(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Ingest a whole tree → {modules, failures, fileCount} contract. */
|
|
239
|
+
export async function ingest(root, { ignore = null } = {}) {
|
|
240
|
+
const files = await walk(root, EXTS, ignore);
|
|
241
|
+
const modules = [];
|
|
242
|
+
const failures = [];
|
|
243
|
+
for (const f of files) {
|
|
244
|
+
const mod = await extractFile(f, root);
|
|
245
|
+
if (mod.failed) failures.push(mod.path);
|
|
246
|
+
else modules.push(mod);
|
|
247
|
+
}
|
|
248
|
+
return { modules, failures, fileCount: files.length };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const meta = { id: "tsc", language: "js/ts", lib: "typescript compiler API", exts: EXTS };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Python extractor — stdlib `ast`, zero npm dependency. Runs extract_ast.py as a
|
|
2
|
+
// subprocess over the whole repo (the script does its own deterministic walk and
|
|
3
|
+
// emits one JSON doc: {modules:[{path,dotted,imports,defines,calls,exports}]} —
|
|
4
|
+
// the same contract the in-process JS/TS extractor emits) and parses the result.
|
|
5
|
+
//
|
|
6
|
+
// Requires a `python3` interpreter at index time (TMCT_PYTHON overrides). This is
|
|
7
|
+
// a runtime tool, not an npm dependency — no parser library ships. When no .py
|
|
8
|
+
// files are present the subprocess never runs; when python3 is missing but .py
|
|
9
|
+
// files exist, the backend degrades: it skips them and reports the count as
|
|
10
|
+
// failures rather than crashing the whole index (JS/TS still produces its graph).
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { walk, relPath } from "./walk.mjs";
|
|
14
|
+
import { exec } from "./spawn.mjs";
|
|
15
|
+
|
|
16
|
+
const AST_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "extract_ast.py");
|
|
17
|
+
const EXTS = [".py"];
|
|
18
|
+
const PYTHON = () => process.env.TMCT_PYTHON || "python3";
|
|
19
|
+
|
|
20
|
+
/** Ingest every .py file under `root` → {modules, failures, fileCount} contract.
|
|
21
|
+
* `ignore` prunes the presence check; extract_ast.py applies its own SKIP_DIRS
|
|
22
|
+
* when it walks, so the module set follows the script's exclusions. */
|
|
23
|
+
export async function ingest(root, { ignore = null } = {}) {
|
|
24
|
+
const files = await walk(root, EXTS, ignore);
|
|
25
|
+
if (files.length === 0) return { modules: [], failures: [], fileCount: 0 };
|
|
26
|
+
|
|
27
|
+
const python = PYTHON();
|
|
28
|
+
const res = await exec(python, [AST_SCRIPT, root]);
|
|
29
|
+
if (res.code !== 0) {
|
|
30
|
+
const why = res.stderr.trim().split("\n").pop()?.slice(-200) || `exit ${res.code}`;
|
|
31
|
+
process.stderr.write(
|
|
32
|
+
`tmct index: python backend skipped ${files.length} .py file(s) — "${python}" unavailable or failed (${why})\n`,
|
|
33
|
+
);
|
|
34
|
+
return { modules: [], failures: files.map((f) => relPath(root, f)), fileCount: files.length };
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try { parsed = JSON.parse(res.stdout); }
|
|
38
|
+
catch {
|
|
39
|
+
process.stderr.write(`tmct index: python backend produced non-JSON (is "${python}" a Python 3.9+ interpreter?)\n`);
|
|
40
|
+
return { modules: [], failures: files.map((f) => relPath(root, f)), fileCount: files.length };
|
|
41
|
+
}
|
|
42
|
+
const modules = Array.isArray(parsed?.modules) ? parsed.modules : [];
|
|
43
|
+
return { modules, failures: [], fileCount: files.length };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const meta = { id: "ast", language: "python", lib: "python stdlib ast", exts: EXTS };
|