tina4-nodejs 3.13.27 → 3.13.29
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 +1 -1
- package/package.json +1 -1
- package/packages/core/src/docs.ts +69 -2
package/CLAUDE.md
CHANGED
package/package.json
CHANGED
|
@@ -931,12 +931,59 @@ export class Docs {
|
|
|
931
931
|
return results.slice(0, Math.max(1, k));
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
+
/**
|
|
935
|
+
* Resolve a class by exact FQN, documented public import path, or bare name.
|
|
936
|
+
*
|
|
937
|
+
* Node stores the bare class name as the FQN (`Database`), but a developer
|
|
938
|
+
* reading the docs may type the published path (`@tina4/orm.Database`,
|
|
939
|
+
* `orm/Database`) or just `Database`. Match exactly first, then by class
|
|
940
|
+
* name (last path segment), disambiguating by requiring the given segments
|
|
941
|
+
* to appear in the stored FQN/file (framework + shortest wins). Unknown
|
|
942
|
+
* names stay `null` — no false positives.
|
|
943
|
+
*/
|
|
944
|
+
private resolveClassEntry(given: string): InternalEntry | null {
|
|
945
|
+
// 1. exact stored key.
|
|
946
|
+
const exact = this.indexCache!.get(given);
|
|
947
|
+
if (exact && exact.kind === "class") return exact;
|
|
948
|
+
|
|
949
|
+
// Split the request on path/namespace separators (`.`, `/`, `\`), dropping
|
|
950
|
+
// a leading scope marker like `@tina4`.
|
|
951
|
+
const gsegs = given.split(/[./\\]+/).filter((s) => s && s !== "@tina4" && !s.startsWith("@"));
|
|
952
|
+
const gname = (gsegs.length ? gsegs[gsegs.length - 1] : given).toLowerCase();
|
|
953
|
+
|
|
954
|
+
const classes: InternalEntry[] = [];
|
|
955
|
+
for (const e of this.indexCache!.values()) {
|
|
956
|
+
if (e.kind === "class") classes.push(e);
|
|
957
|
+
}
|
|
958
|
+
const cands = classes.filter((e) => e.name.toLowerCase() === gname);
|
|
959
|
+
if (cands.length === 1) return cands[0]; // 2a. unique class-name match
|
|
960
|
+
if (cands.length === 0) return null;
|
|
961
|
+
|
|
962
|
+
// 2b. disambiguate by segment subset — the given dotted/slashed segments
|
|
963
|
+
// must all appear in the stored fqn or file path. Prefer framework, then
|
|
964
|
+
// the shortest fqn, then lexical order.
|
|
965
|
+
const lowSegs = gsegs.map((s) => s.toLowerCase());
|
|
966
|
+
const subset = cands.filter((e) => {
|
|
967
|
+
const hay = `${e.fqn} ${e.file}`.toLowerCase().split(/[./\\\s]+/).filter(Boolean);
|
|
968
|
+
return lowSegs.every((s) => hay.includes(s));
|
|
969
|
+
});
|
|
970
|
+
const pool = subset.length ? subset : cands;
|
|
971
|
+
pool.sort((a, b) => {
|
|
972
|
+
const fa = a.source === "framework" ? 0 : 1;
|
|
973
|
+
const fb = b.source === "framework" ? 0 : 1;
|
|
974
|
+
if (fa !== fb) return fa - fb;
|
|
975
|
+
if (a.fqn.length !== b.fqn.length) return a.fqn.length - b.fqn.length;
|
|
976
|
+
return a.fqn.localeCompare(b.fqn);
|
|
977
|
+
});
|
|
978
|
+
return pool[0];
|
|
979
|
+
}
|
|
980
|
+
|
|
934
981
|
/**
|
|
935
982
|
* Return the full spec for a single class, or `null` if not found.
|
|
936
983
|
*/
|
|
937
984
|
classSpec(fqn: string): ClassSpec | null {
|
|
938
985
|
this.ensureIndex();
|
|
939
|
-
const cls = this.
|
|
986
|
+
const cls = this.resolveClassEntry(fqn);
|
|
940
987
|
if (!cls || cls.kind !== "class") return null;
|
|
941
988
|
const methods: ClassSpec["methods"] = [];
|
|
942
989
|
const prefix = `${cls.fqn}.`;
|
|
@@ -980,7 +1027,7 @@ export class Docs {
|
|
|
980
1027
|
*/
|
|
981
1028
|
methodSpec(classFqn: string, methodName: string): MethodSpec | null {
|
|
982
1029
|
this.ensureIndex();
|
|
983
|
-
const cls = this.
|
|
1030
|
+
const cls = this.resolveClassEntry(classFqn);
|
|
984
1031
|
if (!cls) return null;
|
|
985
1032
|
const key = `${cls.fqn}.${methodName}`;
|
|
986
1033
|
const entry = this.indexCache!.get(key);
|
|
@@ -1184,6 +1231,26 @@ export class Docs {
|
|
|
1184
1231
|
for (const tk of tokens) {
|
|
1185
1232
|
if (tk && doc.includes(tk)) score += 1;
|
|
1186
1233
|
}
|
|
1234
|
+
// Class-qualified queries ("Frond.addTest" / "Frond addTest"): score the
|
|
1235
|
+
// owning class so the qualifier steers ranking instead of being dead weight.
|
|
1236
|
+
const parent = (entry.class ?? "").toLowerCase();
|
|
1237
|
+
if (parent) {
|
|
1238
|
+
// Normalise `.`/`:`/whitespace in the joined query to a single `.` so
|
|
1239
|
+
// "frond.addtest", "frond:addtest" and "frondaddtest" all compare alike.
|
|
1240
|
+
const qNorm = joined.replace(/[:.]+/g, ".");
|
|
1241
|
+
if (qNorm === `${parent}.${name}` || qNorm === `${parent}.${stripped}`) {
|
|
1242
|
+
score += 6; // exact "Class.method" intent — the strongest signal
|
|
1243
|
+
}
|
|
1244
|
+
for (const tk of tokens) {
|
|
1245
|
+
if (tk === parent) score += 2.5;
|
|
1246
|
+
else if (tk && parent.startsWith(tk)) score += 1;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// Any token that is a whole segment of the fqn (module / class / name).
|
|
1250
|
+
const fqnSegs = new Set(entry.fqn.toLowerCase().split(/[.\s:]+/).filter(Boolean));
|
|
1251
|
+
for (const tk of tokens) {
|
|
1252
|
+
if (tk && fqnSegs.has(tk)) score += 1;
|
|
1253
|
+
}
|
|
1187
1254
|
if (joined && score === 0 && name.includes(joined)) score += 2;
|
|
1188
1255
|
return score;
|
|
1189
1256
|
}
|