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 CHANGED
@@ -1,4 +1,4 @@
1
- # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.27)
1
+ # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.29)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- "version": "3.13.27",
6
+ "version": "3.13.29",
7
7
 
8
8
  "type": "module",
9
9
  "description": "Tina4 for Node.js/TypeScript \u2014 54 built-in features, zero dependencies",
@@ -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.indexCache!.get(fqn);
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.indexCache!.get(classFqn);
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
  }