openings 0.1.41 → 0.1.43

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openings",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "Find evidence-grounded jobs, including relevant roles you may not have searched for, without accounts or API keys.",
5
5
  "author": { "name": "Openings contributors" },
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openings",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "A free, candidate-safe job-search substrate for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/abhay-avagama/hiring-agent.git" },
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@ export const companies: Company[] = Object.entries(companyData).map(([slug, valu
16
16
  }));
17
17
 
18
18
  export const catalog = createCatalog({ companies });
19
- export { createCatalog } from "./catalog.ts";
19
+ export { createCatalog, searchJobs } from "./catalog.ts";
20
20
  export { EXPERIENCE_VERSION, experienceLabel, experienceMatches, normalizeJobExperience, statedExperience, titleExperience, type Experience } from "./experience.ts";
21
21
  export { matchesSearchTerms, searchHaystack, searchTerms, searchTokens } from "./text-match.ts";
22
22
  export type { Catalog } from "./catalog.ts";
package/src/text-match.ts CHANGED
@@ -6,12 +6,34 @@
6
6
  /** Everything outside this set separates tokens, so "full-stack" and "full stack" agree. */
7
7
  const SEPARATORS = /[^a-z0-9.+#]+/g;
8
8
 
9
- /** A trailing plural "s" is dropped on both sides, so "engineers" and "engineer" meet in the middle. */
9
+ /**
10
+ * Word forms are reduced on both sides, because a candidate typing "engineer" means "Engineering" too.
11
+ * A first version dropped only plurals, and a coverage panel measured the cost: of 598 results it removed,
12
+ * roughly 42 were genuine false matches and the rest were real roles whose titles used another form.
13
+ * Suffixes come off longest first, then a trailing "e", so engineer/engineering and develop/developer/development
14
+ * all land on one stem. Technology names carrying + or # are never touched.
15
+ */
16
+ const SUFFIXES = ["ment", "ing", "or", "er", "s"];
10
17
  function stem(token: string): string {
11
18
  if (token.length < 4 || /[+#]/.test(token)) return token;
12
- return token.endsWith("s") && !token.endsWith("ss") ? token.slice(0, -1) : token;
19
+ let word = token;
20
+ // Reduce repeatedly, or "engineering" stops at "engineer" while "engineer" goes on to "engine" and the two never meet.
21
+ for (let pass = 0; pass < 3; pass += 1) {
22
+ const before = word;
23
+ for (const suffix of SUFFIXES) {
24
+ if (word.length - suffix.length >= 4 && word.endsWith(suffix) && !(suffix === "s" && word.endsWith("ss"))) {
25
+ word = word.slice(0, -suffix.length);
26
+ break;
27
+ }
28
+ }
29
+ if (word === before) break;
30
+ }
31
+ return word.length > 4 && word.endsWith("e") ? word.slice(0, -1) : word;
13
32
  }
14
33
 
34
+ /** Words written as one in some titles and two in others; the index carries both readings. */
35
+ const COMPOUNDS = [["full", "stack"], ["front", "end"], ["back", "end"], ["dev", "ops"], ["data", "base"], ["work", "flow"]].map(([left, right]) => ({ left: left!, right: right!, joined: `${left}${right}` }));
36
+
15
37
  export function searchTokens(text: string): string[] {
16
38
  return text.toLocaleLowerCase().replace(SEPARATORS, " ").split(" ")
17
39
  .map((token) => token.replace(/^\.+/, "").replace(/\.+$/, ""))
@@ -22,10 +44,16 @@ export function searchTokens(text: string): string[] {
22
44
  /** The text a job is matched against, padded so a term only matches a whole token. A dotted name is also
23
45
  * indexed by its parts, so "node" finds "Node.js" while "node.js" still does. */
24
46
  export function searchHaystack(text: string): string {
25
- const indexed = new Set<string>();
26
- for (const token of searchTokens(text)) {
27
- indexed.add(token);
28
- if (token.includes(".")) for (const part of token.split(".")) if (part) indexed.add(stem(part));
47
+ const tokens = searchTokens(text);
48
+ const indexed = new Set<string>(tokens);
49
+ for (const token of tokens) if (token.includes(".")) for (const part of token.split(".")) if (part) indexed.add(stem(part));
50
+ // "Fullstack" and "Full Stack" are the same job: index each reading so either spelling finds both.
51
+ for (const { left, right, joined } of COMPOUNDS) {
52
+ const leftStem = stem(left); const rightStem = stem(right); const joinedStem = stem(joined);
53
+ if (indexed.has(joinedStem)) { indexed.add(leftStem); indexed.add(rightStem); }
54
+ for (let index = 0; index + 1 < tokens.length; index += 1) {
55
+ if (tokens[index] === leftStem && tokens[index + 1] === rightStem) indexed.add(joinedStem);
56
+ }
29
57
  }
30
58
  return ` ${[...indexed].join(" ")} `;
31
59
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.41";
1
+ export const VERSION = "0.1.43";