tina4-nodejs 3.13.133 → 3.13.134
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/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +3181 -3051
- package/packages/cli/src/commands/generate.ts +33 -22
- package/packages/cli/src/commands/lint.ts +77 -111
- package/packages/core/dist/index.js +3090 -2952
- package/packages/core/src/.tina4-metrics.json +15004 -0
- package/packages/core/src/aiClient.ts +199 -161
- package/packages/core/src/dispatchPipeline.ts +65 -67
- package/packages/core/src/docs.ts +52 -544
- package/packages/core/src/docsParser.ts +270 -0
- package/packages/core/src/docsScanner.ts +121 -0
- package/packages/core/src/docsSignatures.ts +165 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/logger.ts +68 -82
- package/packages/core/src/mcp.ts +32 -60
- package/packages/core/src/messenger.ts +136 -157
- package/packages/core/src/middleware.ts +56 -60
- package/packages/core/src/plan.ts +78 -70
- package/packages/core/src/projectIndex.ts +15 -288
- package/packages/core/src/projectIndexExtractors.ts +126 -0
- package/packages/core/src/projectIndexStorage.ts +122 -0
- package/packages/core/src/push.ts +281 -0
- package/packages/core/src/server.ts +182 -183
- package/packages/frond/dist/index.js +607 -770
- package/packages/frond/src/engine.ts +670 -818
- package/packages/orm/dist/index.js +3100 -2965
- package/packages/orm/src/adapters/mongodb.ts +99 -144
- package/packages/orm/src/baseModel.ts +429 -515
- package/packages/orm/src/fakeData.ts +73 -61
- package/packages/orm/src/migration.ts +96 -126
- package/packages/orm/src/seeder.ts +6 -238
- package/packages/orm/src/seederTable.ts +101 -0
- package/packages/orm/src/seederTypes.ts +14 -0
- package/packages/orm/src/validation.ts +97 -80
- package/types/core/src/aiClient.d.ts +5 -0
- package/types/core/src/docsParser.d.ts +28 -0
- package/types/core/src/docsScanner.d.ts +1 -0
- package/types/core/src/docsSignatures.d.ts +11 -0
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/messenger.d.ts +8 -0
- package/types/core/src/projectIndexExtractors.d.ts +3 -0
- package/types/core/src/projectIndexStorage.d.ts +13 -0
- package/types/core/src/push.d.ts +45 -0
- package/types/frond/src/engine.d.ts +25 -0
- package/types/orm/src/fakeData.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +3 -89
- package/types/orm/src/seederTable.d.ts +9 -0
- package/types/orm/src/seederTypes.d.ts +16 -0
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import * as fs from "node:fs";
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { parseTypeScript, type ParsedFile } from "./docsParser.js";
|
|
22
23
|
|
|
23
24
|
// ── Types ────────────────────────────────────────────────────────────
|
|
24
25
|
|
|
@@ -223,505 +224,6 @@ function docblockBody(doc: string): string {
|
|
|
223
224
|
return lines.join(" ");
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
// ── TS regex parser ──────────────────────────────────────────────────
|
|
227
|
-
|
|
228
|
-
interface ParsedClass {
|
|
229
|
-
name: string;
|
|
230
|
-
line: number;
|
|
231
|
-
doc: string;
|
|
232
|
-
exported: boolean;
|
|
233
|
-
methods: ParsedMethod[];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
interface ParsedMethod {
|
|
237
|
-
name: string;
|
|
238
|
-
line: number;
|
|
239
|
-
doc: string;
|
|
240
|
-
signature: string;
|
|
241
|
-
visibility: "public" | "protected" | "private";
|
|
242
|
-
static: boolean;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
interface ParsedFile {
|
|
246
|
-
classes: ParsedClass[];
|
|
247
|
-
functions: ParsedMethod[];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const CLASS_RE = /(?:^|\n)([ \t]*)((?:export\s+(?:default\s+)?(?:abstract\s+)?)?class\s+([A-Za-z_$][\w$]*))[\s\S]*?(?=\n[ \t]*(?:export\s+(?:default\s+)?(?:abstract\s+)?class|export\s+function|function|$))/g;
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Parse a TS source string. Lightweight — finds top-level classes and their
|
|
254
|
-
* public methods, plus top-level exported functions. Captures preceding JSDoc.
|
|
255
|
-
*
|
|
256
|
-
* Strategy: scan token-by-token. We don't need a full AST — we only care
|
|
257
|
-
* about identifying class declarations, brace depth (to find class members),
|
|
258
|
-
* method/function declarations, and JSDoc comments immediately above.
|
|
259
|
-
*/
|
|
260
|
-
function parseTypeScript(source: string, _debugTag = ""): ParsedFile {
|
|
261
|
-
const classes: ParsedClass[] = [];
|
|
262
|
-
const functions: ParsedMethod[] = [];
|
|
263
|
-
|
|
264
|
-
// Strip line comments and string contents (preserve length for line numbers).
|
|
265
|
-
const stripped = stripStrings(source);
|
|
266
|
-
const lines = source.split(/\r?\n/);
|
|
267
|
-
|
|
268
|
-
let i = 0;
|
|
269
|
-
let line = 1;
|
|
270
|
-
let pendingDoc = "";
|
|
271
|
-
let pendingDocLine = 0;
|
|
272
|
-
const len = stripped.length;
|
|
273
|
-
let braceDepth = 0;
|
|
274
|
-
let classStack: { name: string; bodyStartDepth: number; entry: ParsedClass; isExport: boolean }[] = [];
|
|
275
|
-
|
|
276
|
-
function lineOf(offset: number): number {
|
|
277
|
-
// Count newlines up to offset.
|
|
278
|
-
let l = 1;
|
|
279
|
-
for (let k = 0; k < offset && k < source.length; k++) {
|
|
280
|
-
if (source.charCodeAt(k) === 10) l++;
|
|
281
|
-
}
|
|
282
|
-
return l;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
while (i < len) {
|
|
286
|
-
const ch = stripped[i];
|
|
287
|
-
|
|
288
|
-
// JSDoc detection
|
|
289
|
-
if (ch === "/" && stripped[i + 1] === "*" && stripped[i + 2] === "*") {
|
|
290
|
-
const end = stripped.indexOf("*/", i + 3);
|
|
291
|
-
if (end === -1) break;
|
|
292
|
-
pendingDoc = source.slice(i, end + 2);
|
|
293
|
-
pendingDocLine = lineOf(i);
|
|
294
|
-
i = end + 2;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Skip line comments (already stripped → '/' followed by '/' won't appear in stripped, but be safe)
|
|
299
|
-
if (ch === "/" && stripped[i + 1] === "/") {
|
|
300
|
-
while (i < len && stripped[i] !== "\n") i++;
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Brace tracking (only outside strings; strings are zeroed in stripped)
|
|
305
|
-
if (ch === "{") {
|
|
306
|
-
braceDepth++;
|
|
307
|
-
i++;
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
if (ch === "}") {
|
|
311
|
-
braceDepth--;
|
|
312
|
-
// Pop classes whose body just closed
|
|
313
|
-
while (classStack.length > 0 && braceDepth <= classStack[classStack.length - 1].bodyStartDepth) {
|
|
314
|
-
const cls = classStack.pop()!;
|
|
315
|
-
classes.push(cls.entry);
|
|
316
|
-
}
|
|
317
|
-
i++;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Look for "class <Name>"
|
|
322
|
-
if (isWordBoundary(stripped, i) && matchKeyword(stripped, i, "class")) {
|
|
323
|
-
// Read modifiers backwards on this line — already covered by pendingDoc capture.
|
|
324
|
-
const after = i + "class".length;
|
|
325
|
-
const nameMatch = /^\s+([A-Za-z_$][\w$]*)/.exec(stripped.slice(after));
|
|
326
|
-
if (nameMatch) {
|
|
327
|
-
const name = nameMatch[1];
|
|
328
|
-
const classLine = lineOf(i);
|
|
329
|
-
// Look for opening '{' starting from after the name
|
|
330
|
-
let j = after + nameMatch[0].length;
|
|
331
|
-
while (j < len && stripped[j] !== "{") j++;
|
|
332
|
-
if (j < len) {
|
|
333
|
-
const isExport = isExportedAt(stripped, lines, classLine, name);
|
|
334
|
-
const entry: ParsedClass = {
|
|
335
|
-
name,
|
|
336
|
-
line: classLine,
|
|
337
|
-
doc: pendingDoc,
|
|
338
|
-
exported: isExport,
|
|
339
|
-
methods: [],
|
|
340
|
-
};
|
|
341
|
-
// Push class context with its bodyStartDepth = current braceDepth
|
|
342
|
-
// (the upcoming '{' will increment braceDepth one above this).
|
|
343
|
-
classStack.push({ name, bodyStartDepth: braceDepth, entry, isExport });
|
|
344
|
-
pendingDoc = "";
|
|
345
|
-
// Move past '{' and increment depth
|
|
346
|
-
braceDepth++;
|
|
347
|
-
i = j + 1;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Method or top-level function detection — we only care about either:
|
|
354
|
-
// * methods inside a class body (classStack non-empty AND directly inside class body)
|
|
355
|
-
// * top-level "export function" or "function" declarations
|
|
356
|
-
if (classStack.length > 0
|
|
357
|
-
&& braceDepth === classStack[classStack.length - 1].bodyStartDepth + 1) {
|
|
358
|
-
// We're directly inside a class body.
|
|
359
|
-
const m = matchMethodSignature(stripped, source, i);
|
|
360
|
-
if (m) {
|
|
361
|
-
const methodLine = lineOf(m.nameStart ?? i);
|
|
362
|
-
const cls = classStack[classStack.length - 1];
|
|
363
|
-
// Skip private/protected based on TS modifier OR name prefix '_'
|
|
364
|
-
const visibility = m.visibility;
|
|
365
|
-
cls.entry.methods.push({
|
|
366
|
-
name: m.name,
|
|
367
|
-
line: methodLine,
|
|
368
|
-
doc: pendingDoc,
|
|
369
|
-
signature: m.signature,
|
|
370
|
-
visibility,
|
|
371
|
-
static: m.static,
|
|
372
|
-
});
|
|
373
|
-
pendingDoc = "";
|
|
374
|
-
i = m.endIndex;
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
} else if (braceDepth === 0) {
|
|
378
|
-
// Top-level — look for "export function" or "function"
|
|
379
|
-
const f = matchTopLevelFunction(stripped, source, i);
|
|
380
|
-
if (f) {
|
|
381
|
-
const fLine = lineOf(f.nameStart ?? i);
|
|
382
|
-
functions.push({
|
|
383
|
-
name: f.name,
|
|
384
|
-
line: fLine,
|
|
385
|
-
doc: pendingDoc,
|
|
386
|
-
signature: f.signature,
|
|
387
|
-
visibility: "public",
|
|
388
|
-
static: false,
|
|
389
|
-
});
|
|
390
|
-
pendingDoc = "";
|
|
391
|
-
i = f.endIndex;
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Whitespace doesn't reset pendingDoc — but most other tokens do.
|
|
397
|
-
if (!/\s/.test(ch)) {
|
|
398
|
-
// Non-whitespace, non-doc-comment — only reset doc if it was a long way back.
|
|
399
|
-
// Be conservative: only reset on punctuation that clearly terminates.
|
|
400
|
-
if (ch === ";") {
|
|
401
|
-
pendingDoc = "";
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
i++;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Any unclosed class (shouldn't happen in valid TS) → flush.
|
|
409
|
-
while (classStack.length > 0) classes.push(classStack.pop()!.entry);
|
|
410
|
-
|
|
411
|
-
// Suppress unused-warning
|
|
412
|
-
void pendingDocLine;
|
|
413
|
-
|
|
414
|
-
return { classes, functions };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function isWordBoundary(text: string, i: number): boolean {
|
|
418
|
-
if (i === 0) return true;
|
|
419
|
-
const prev = text.charCodeAt(i - 1);
|
|
420
|
-
// Word chars: A-Z a-z 0-9 _ $
|
|
421
|
-
if ((prev >= 65 && prev <= 90) || (prev >= 97 && prev <= 122) || (prev >= 48 && prev <= 57) || prev === 95 || prev === 36) {
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
return true;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function matchKeyword(text: string, i: number, kw: string): boolean {
|
|
428
|
-
if (text.substr(i, kw.length) !== kw) return false;
|
|
429
|
-
const after = i + kw.length;
|
|
430
|
-
if (after >= text.length) return true;
|
|
431
|
-
const nextCode = text.charCodeAt(after);
|
|
432
|
-
if ((nextCode >= 65 && nextCode <= 90) || (nextCode >= 97 && nextCode <= 122) || (nextCode >= 48 && nextCode <= 57) || nextCode === 95 || nextCode === 36) {
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function isExportedAt(_stripped: string, lines: string[], lineNo: number, name: string): boolean {
|
|
439
|
-
// Walk back up to 8 lines and look for "export class <name>" / "export default class <name>"
|
|
440
|
-
const start = Math.max(0, lineNo - 1);
|
|
441
|
-
const exportClass = "export class " + name;
|
|
442
|
-
const exportDefaultClass = "export default class " + name;
|
|
443
|
-
const exportAbstractClass = "export abstract class " + name;
|
|
444
|
-
const justClass = "class " + name;
|
|
445
|
-
for (let l = start; l >= Math.max(0, start - 8); l--) {
|
|
446
|
-
const ln = lines[l] || "";
|
|
447
|
-
if (ln.includes(exportClass) || ln.includes(exportDefaultClass) || ln.includes(exportAbstractClass)) {
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
if (ln.includes(justClass)) {
|
|
451
|
-
// declared but not exported
|
|
452
|
-
return /\bexport\b/.test(ln);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
interface MethodMatch {
|
|
459
|
-
name: string;
|
|
460
|
-
signature: string;
|
|
461
|
-
endIndex: number;
|
|
462
|
-
nameStart: number;
|
|
463
|
-
visibility: "public" | "protected" | "private";
|
|
464
|
-
static: boolean;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const METHOD_HEAD_RE =
|
|
468
|
-
/^([ \t]*)((?:public|protected|private|readonly|static|async|abstract|override|\s)*)([A-Za-z_$][\w$]*)\s*[<(]/;
|
|
469
|
-
|
|
470
|
-
function matchMethodSignature(stripped: string, source: string, i: number): MethodMatch | null {
|
|
471
|
-
// Method must be at start-of-line-ish position.
|
|
472
|
-
if (i > 0) {
|
|
473
|
-
const prev = stripped.charCodeAt(i - 1);
|
|
474
|
-
if (prev !== 10 && prev !== 32 && prev !== 9 && prev !== 123) return null;
|
|
475
|
-
}
|
|
476
|
-
// Take the rest of the current line + a little ahead.
|
|
477
|
-
let lineEnd = stripped.indexOf("\n", i);
|
|
478
|
-
if (lineEnd === -1) lineEnd = stripped.length;
|
|
479
|
-
// Read up to 4 lines for multi-line signatures.
|
|
480
|
-
let chunkEnd = lineEnd;
|
|
481
|
-
for (let extra = 0; extra < 4 && chunkEnd < stripped.length; extra++) {
|
|
482
|
-
const next = stripped.indexOf("\n", chunkEnd + 1);
|
|
483
|
-
if (next === -1) break;
|
|
484
|
-
chunkEnd = next;
|
|
485
|
-
}
|
|
486
|
-
const chunk = stripped.slice(i, chunkEnd + 1);
|
|
487
|
-
const match = METHOD_HEAD_RE.exec(chunk);
|
|
488
|
-
if (!match) return null;
|
|
489
|
-
const modifiers = match[2] || "";
|
|
490
|
-
const name = match[3];
|
|
491
|
-
// Skip reserved words / control-flow that masquerade as method names.
|
|
492
|
-
const reserved = new Set([
|
|
493
|
-
"if", "for", "while", "switch", "return", "do", "try", "catch", "throw",
|
|
494
|
-
"const", "let", "var", "import", "export", "function", "class", "interface",
|
|
495
|
-
"type", "new", "yield", "await", "case", "break", "continue", "else",
|
|
496
|
-
]);
|
|
497
|
-
if (reserved.has(name)) return null;
|
|
498
|
-
|
|
499
|
-
// Determine visibility from modifiers
|
|
500
|
-
let visibility: "public" | "protected" | "private" = "public";
|
|
501
|
-
if (/\bprivate\b/.test(modifiers)) visibility = "private";
|
|
502
|
-
else if (/\bprotected\b/.test(modifiers)) visibility = "protected";
|
|
503
|
-
const isStatic = /\bstatic\b/.test(modifiers);
|
|
504
|
-
|
|
505
|
-
// Capture signature — read from start of "name" up through matching ')' and optional return type.
|
|
506
|
-
const nameStart = i + match[1].length + match[2].length;
|
|
507
|
-
const result = captureSignature(stripped, source, nameStart, name);
|
|
508
|
-
if (!result) return null;
|
|
509
|
-
|
|
510
|
-
return {
|
|
511
|
-
name,
|
|
512
|
-
signature: result.signature,
|
|
513
|
-
endIndex: result.endIndex,
|
|
514
|
-
nameStart,
|
|
515
|
-
visibility,
|
|
516
|
-
static: isStatic,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const FN_HEAD_RE =
|
|
521
|
-
/^((?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+)([A-Za-z_$][\w$]*)\s*[<(]/;
|
|
522
|
-
|
|
523
|
-
function matchTopLevelFunction(stripped: string, source: string, i: number): MethodMatch | null {
|
|
524
|
-
if (i > 0) {
|
|
525
|
-
const prev = stripped.charCodeAt(i - 1);
|
|
526
|
-
if (prev !== 10 && prev !== 32 && prev !== 9) return null;
|
|
527
|
-
}
|
|
528
|
-
let lineEnd = stripped.indexOf("\n", i);
|
|
529
|
-
if (lineEnd === -1) lineEnd = stripped.length;
|
|
530
|
-
let chunkEnd = lineEnd;
|
|
531
|
-
for (let extra = 0; extra < 4 && chunkEnd < stripped.length; extra++) {
|
|
532
|
-
const next = stripped.indexOf("\n", chunkEnd + 1);
|
|
533
|
-
if (next === -1) break;
|
|
534
|
-
chunkEnd = next;
|
|
535
|
-
}
|
|
536
|
-
const chunk = stripped.slice(i, chunkEnd + 1);
|
|
537
|
-
const match = FN_HEAD_RE.exec(chunk);
|
|
538
|
-
if (!match) return null;
|
|
539
|
-
const name = match[2];
|
|
540
|
-
const nameStart = i + match[1].length;
|
|
541
|
-
const result = captureSignature(stripped, source, nameStart, name);
|
|
542
|
-
if (!result) return null;
|
|
543
|
-
return {
|
|
544
|
-
name,
|
|
545
|
-
signature: result.signature,
|
|
546
|
-
endIndex: result.endIndex,
|
|
547
|
-
nameStart,
|
|
548
|
-
visibility: "public",
|
|
549
|
-
static: false,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
interface CapturedSig {
|
|
554
|
-
signature: string;
|
|
555
|
-
endIndex: number;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function captureSignature(stripped: string, source: string, nameStart: number, name: string): CapturedSig | null {
|
|
559
|
-
// Skip the name
|
|
560
|
-
let j = nameStart + name.length;
|
|
561
|
-
// Optional generic <...>
|
|
562
|
-
while (j < stripped.length && /\s/.test(stripped[j])) j++;
|
|
563
|
-
if (stripped[j] === "<") {
|
|
564
|
-
let depth = 0;
|
|
565
|
-
while (j < stripped.length) {
|
|
566
|
-
const c = stripped[j];
|
|
567
|
-
if (c === "<") depth++;
|
|
568
|
-
else if (c === ">") {
|
|
569
|
-
depth--;
|
|
570
|
-
if (depth === 0) { j++; break; }
|
|
571
|
-
}
|
|
572
|
-
j++;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
// Whitespace
|
|
576
|
-
while (j < stripped.length && /\s/.test(stripped[j])) j++;
|
|
577
|
-
if (stripped[j] !== "(") return null;
|
|
578
|
-
// Capture (...) balanced on parens (ignore strings — already stripped)
|
|
579
|
-
const parenStart = j;
|
|
580
|
-
let depth = 0;
|
|
581
|
-
while (j < stripped.length) {
|
|
582
|
-
const c = stripped[j];
|
|
583
|
-
if (c === "(") depth++;
|
|
584
|
-
else if (c === ")") {
|
|
585
|
-
depth--;
|
|
586
|
-
if (depth === 0) { j++; break; }
|
|
587
|
-
}
|
|
588
|
-
j++;
|
|
589
|
-
}
|
|
590
|
-
const parenSegment = source.slice(parenStart, j); // pull from original source for human-readable
|
|
591
|
-
// Optional return type: ": Type" up to '{' or ';' or '=>' or end-of-line for arrow.
|
|
592
|
-
let retStart = j;
|
|
593
|
-
while (retStart < stripped.length && /[ \t]/.test(stripped[retStart])) retStart++;
|
|
594
|
-
let retEnd = retStart;
|
|
595
|
-
let returnType = "";
|
|
596
|
-
if (stripped[retStart] === ":") {
|
|
597
|
-
retEnd = retStart + 1;
|
|
598
|
-
let depthBracket = 0;
|
|
599
|
-
while (retEnd < stripped.length) {
|
|
600
|
-
const c = stripped[retEnd];
|
|
601
|
-
// Stop on a body-opening '{' or terminating ';' at the same depth.
|
|
602
|
-
if (depthBracket === 0 && (c === "{" || c === ";")) break;
|
|
603
|
-
if (depthBracket === 0 && c === "\n") {
|
|
604
|
-
// Arrow-return on next line — stop only if the next non-space is '{'.
|
|
605
|
-
let k2 = retEnd + 1;
|
|
606
|
-
while (k2 < stripped.length && (stripped[k2] === " " || stripped[k2] === "\t")) k2++;
|
|
607
|
-
if (stripped[k2] === "{") break;
|
|
608
|
-
}
|
|
609
|
-
if (c === "<" || c === "(" || c === "[") depthBracket++;
|
|
610
|
-
else if (c === ">" || c === ")" || c === "]") depthBracket--;
|
|
611
|
-
retEnd++;
|
|
612
|
-
}
|
|
613
|
-
returnType = source.slice(retStart, retEnd).trim();
|
|
614
|
-
}
|
|
615
|
-
const cleanedParens = parenSegment.replace(/\s+/g, " ");
|
|
616
|
-
const sig = name + cleanedParens + (returnType ? " " + returnType : "");
|
|
617
|
-
return { signature: sig, endIndex: retEnd };
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Replace string literals and template literal contents with spaces of equal
|
|
622
|
-
* length so brace/paren scanning isn't fooled by characters inside strings.
|
|
623
|
-
* Also strips line + block comments. Newlines are preserved so line numbers
|
|
624
|
-
* line up with the original source.
|
|
625
|
-
*/
|
|
626
|
-
function stripStrings(source: string): string {
|
|
627
|
-
const out: string[] = [];
|
|
628
|
-
const len = source.length;
|
|
629
|
-
let i = 0;
|
|
630
|
-
const BACKTICK = String.fromCharCode(96);
|
|
631
|
-
const DOLLAR = "$";
|
|
632
|
-
const OPEN_BRACE = "{";
|
|
633
|
-
const CLOSE_BRACE = "}";
|
|
634
|
-
|
|
635
|
-
while (i < len) {
|
|
636
|
-
const c = source[i];
|
|
637
|
-
|
|
638
|
-
if (c === "/" && source[i + 1] === "*") {
|
|
639
|
-
const end = source.indexOf("*/", i + 2);
|
|
640
|
-
if (end === -1) {
|
|
641
|
-
for (let k = i; k < len; k++) out.push(source[k] === "\n" ? "\n" : " ");
|
|
642
|
-
return out.join("");
|
|
643
|
-
}
|
|
644
|
-
for (let k = i; k < end + 2; k++) out.push(source[k] === "\n" ? "\n" : " ");
|
|
645
|
-
i = end + 2;
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (c === "/" && source[i + 1] === "/") {
|
|
650
|
-
while (i < len && source[i] !== "\n") {
|
|
651
|
-
out.push(" ");
|
|
652
|
-
i++;
|
|
653
|
-
}
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (c === '"' || c === "'") {
|
|
658
|
-
out.push(c);
|
|
659
|
-
i++;
|
|
660
|
-
while (i < len) {
|
|
661
|
-
const sc = source[i];
|
|
662
|
-
if (sc === "\\" && i + 1 < len) {
|
|
663
|
-
out.push(" ");
|
|
664
|
-
i += 2;
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
if (sc === c) {
|
|
668
|
-
out.push(c);
|
|
669
|
-
i++;
|
|
670
|
-
break;
|
|
671
|
-
}
|
|
672
|
-
out.push(sc === "\n" ? "\n" : " ");
|
|
673
|
-
i++;
|
|
674
|
-
}
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (c === BACKTICK) {
|
|
679
|
-
// Treat the entire template literal — including any ${...} expressions —
|
|
680
|
-
// as opaque content. Replace every char inside with spaces so brace/paren
|
|
681
|
-
// accounting in the outer parser isn't fooled. Newlines preserved.
|
|
682
|
-
out.push(c);
|
|
683
|
-
i++;
|
|
684
|
-
while (i < len) {
|
|
685
|
-
const tc = source[i];
|
|
686
|
-
if (tc === "\\" && i + 1 < len) {
|
|
687
|
-
out.push(" ");
|
|
688
|
-
i += 2;
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
if (tc === BACKTICK) {
|
|
692
|
-
out.push(BACKTICK);
|
|
693
|
-
i++;
|
|
694
|
-
break;
|
|
695
|
-
}
|
|
696
|
-
if (tc === DOLLAR && source[i + 1] === OPEN_BRACE) {
|
|
697
|
-
// Skip over the entire ${ ... } expression, replacing all chars with
|
|
698
|
-
// spaces (or newlines). Track real brace depth (ignoring nested
|
|
699
|
-
// template literals' own braces, but those are zeroed too).
|
|
700
|
-
out.push(" ");
|
|
701
|
-
out.push(" ");
|
|
702
|
-
i += 2;
|
|
703
|
-
let depth = 1;
|
|
704
|
-
while (i < len && depth > 0) {
|
|
705
|
-
const ic = source[i];
|
|
706
|
-
if (ic === OPEN_BRACE) depth++;
|
|
707
|
-
else if (ic === CLOSE_BRACE) depth--;
|
|
708
|
-
out.push(ic === "\n" ? "\n" : " ");
|
|
709
|
-
i++;
|
|
710
|
-
}
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
out.push(tc === "\n" ? "\n" : " ");
|
|
714
|
-
i++;
|
|
715
|
-
}
|
|
716
|
-
continue;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
out.push(c);
|
|
720
|
-
i++;
|
|
721
|
-
}
|
|
722
|
-
return out.join("");
|
|
723
|
-
}
|
|
724
|
-
|
|
725
227
|
// ── Indexer ──────────────────────────────────────────────────────────
|
|
726
228
|
|
|
727
229
|
function walkTsFiles(root: string): string[] {
|
|
@@ -870,6 +372,52 @@ function buildLineIndex(text: string): (offset: number) => number {
|
|
|
870
372
|
};
|
|
871
373
|
}
|
|
872
374
|
|
|
375
|
+
function scoreNameToken(name: string, stripped: string, nameTokens: string[], token: string): number {
|
|
376
|
+
if (!token) return 0;
|
|
377
|
+
if (name.startsWith(token) || stripped.startsWith(token)) return 3;
|
|
378
|
+
for (const nameToken of nameTokens) {
|
|
379
|
+
if (nameToken === token) return 3;
|
|
380
|
+
if (nameToken.startsWith(token)) return 2;
|
|
381
|
+
}
|
|
382
|
+
return name.includes(token) ? 0.5 : 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function scoreName(name: string, stripped: string, nameTokens: string[], tokens: string[], joined: string): number {
|
|
386
|
+
let score = name === joined || stripped === joined ? 5 : 0;
|
|
387
|
+
for (const token of tokens) score += scoreNameToken(name, stripped, nameTokens, token);
|
|
388
|
+
return score;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function scoreText(text: string, tokens: string[], weight: number): number {
|
|
392
|
+
let score = 0;
|
|
393
|
+
for (const token of tokens) {
|
|
394
|
+
if (token && text.includes(token)) score += weight;
|
|
395
|
+
}
|
|
396
|
+
return score;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function scoreClassQualifier(entry: InternalEntry, name: string, tokens: string[], joined: string): number {
|
|
400
|
+
const parent = (entry.class ?? "").toLowerCase();
|
|
401
|
+
if (!parent) return 0;
|
|
402
|
+
let score = 0;
|
|
403
|
+
const normalized = joined.replace(/[:.]+/g, ".");
|
|
404
|
+
if (normalized === `${parent}.${name}` || normalized === `${parent}.${name.replace(/^_+/, "")}`) score += 6;
|
|
405
|
+
for (const token of tokens) {
|
|
406
|
+
if (token === parent) score += 2.5;
|
|
407
|
+
else if (token && parent.startsWith(token)) score += 1;
|
|
408
|
+
}
|
|
409
|
+
return score;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function scoreFqn(entry: InternalEntry, tokens: string[]): number {
|
|
413
|
+
const segments = new Set(entry.fqn.toLowerCase().split(/[.\s:]+/).filter(Boolean));
|
|
414
|
+
let score = 0;
|
|
415
|
+
for (const token of tokens) {
|
|
416
|
+
if (token && segments.has(token)) score += 1;
|
|
417
|
+
}
|
|
418
|
+
return score;
|
|
419
|
+
}
|
|
420
|
+
|
|
873
421
|
// ── Public Docs class ───────────────────────────────────────────────
|
|
874
422
|
|
|
875
423
|
/**
|
|
@@ -1233,52 +781,12 @@ export class Docs {
|
|
|
1233
781
|
private scoreEntry(entry: InternalEntry, tokens: string[], joined: string): number {
|
|
1234
782
|
const name = entry.name.toLowerCase();
|
|
1235
783
|
const stripped = name.replace(/^_+/, "");
|
|
1236
|
-
const summary = entry.summary.toLowerCase();
|
|
1237
|
-
const doc = entry.docblock.toLowerCase();
|
|
1238
|
-
let score = 0;
|
|
1239
|
-
|
|
1240
|
-
if (name === joined || stripped === joined) score += 5;
|
|
1241
|
-
|
|
1242
784
|
const nameTokens = tokenise(entry.name);
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1249
|
-
let hit = false;
|
|
1250
|
-
for (const nt of nameTokens) {
|
|
1251
|
-
if (nt === tk) { score += 3; hit = true; break; }
|
|
1252
|
-
if (nt.startsWith(tk)) { score += 2; hit = true; break; }
|
|
1253
|
-
}
|
|
1254
|
-
if (!hit && name.includes(tk)) score += 0.5;
|
|
1255
|
-
}
|
|
1256
|
-
for (const tk of tokens) {
|
|
1257
|
-
if (tk && summary.includes(tk)) score += 2;
|
|
1258
|
-
}
|
|
1259
|
-
for (const tk of tokens) {
|
|
1260
|
-
if (tk && doc.includes(tk)) score += 1;
|
|
1261
|
-
}
|
|
1262
|
-
// Class-qualified queries ("Frond.addTest" / "Frond addTest"): score the
|
|
1263
|
-
// owning class so the qualifier steers ranking instead of being dead weight.
|
|
1264
|
-
const parent = (entry.class ?? "").toLowerCase();
|
|
1265
|
-
if (parent) {
|
|
1266
|
-
// Normalise `.`/`:`/whitespace in the joined query to a single `.` so
|
|
1267
|
-
// "frond.addtest", "frond:addtest" and "frondaddtest" all compare alike.
|
|
1268
|
-
const qNorm = joined.replace(/[:.]+/g, ".");
|
|
1269
|
-
if (qNorm === `${parent}.${name}` || qNorm === `${parent}.${stripped}`) {
|
|
1270
|
-
score += 6; // exact "Class.method" intent — the strongest signal
|
|
1271
|
-
}
|
|
1272
|
-
for (const tk of tokens) {
|
|
1273
|
-
if (tk === parent) score += 2.5;
|
|
1274
|
-
else if (tk && parent.startsWith(tk)) score += 1;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
// Any token that is a whole segment of the fqn (module / class / name).
|
|
1278
|
-
const fqnSegs = new Set(entry.fqn.toLowerCase().split(/[.\s:]+/).filter(Boolean));
|
|
1279
|
-
for (const tk of tokens) {
|
|
1280
|
-
if (tk && fqnSegs.has(tk)) score += 1;
|
|
1281
|
-
}
|
|
785
|
+
let score = scoreName(name, stripped, nameTokens, tokens, joined);
|
|
786
|
+
score += scoreText(entry.summary.toLowerCase(), tokens, 2);
|
|
787
|
+
score += scoreText(entry.docblock.toLowerCase(), tokens, 1);
|
|
788
|
+
score += scoreClassQualifier(entry, name, tokens, joined);
|
|
789
|
+
score += scoreFqn(entry, tokens);
|
|
1282
790
|
if (joined && score === 0 && name.includes(joined)) score += 2;
|
|
1283
791
|
return score;
|
|
1284
792
|
}
|